/* eslint-disable sonarjs/no-duplicate-string */
import FormData from 'form-data';
import { Api as ApiBase, ApiResponseType, DefaultApiException, DefaultResponseProcessor } from 'rest-api-handler';
import Activity from './Activity';
import StravaException from './StravaException';
import * as STREAM from './streams';
import { ActivityFilters } from './types/ActivityFilters';
import { ApiActivity } from './types/api/Activity';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.FormData = FormData;

type Scope = 'read' | 'read_all' | 'profile:read_all' | 'profile:write' | 'activity:read' | 'activity:read_all' | 'activity:write';
type Prompt = 'force' | 'auto';

interface Athlete {
    badge_type_id: number;
    city: string;
    country: string;
    created_at: string;
    email: string;
    firstname: string;
    follower: string | undefined;
    friend: string | undefined;
    id: number;
    lastname: string;
    premium: boolean;
    profile: string;
    profile_medium: string;
    resource_state: number;
    sex: string;
    state: string;
    updated_at: string;
    username: string;
}

interface Token {
    access_token: string;
    athlete: Athlete;
    expires_at: number;
    expires_in: number;
    refresh_token: string;
    token_type: string;
}

interface UploadStatus {
    activity_id: number | undefined;
    error: string | undefined;
    external_id: string;
    id: number;
    status: string;
}

function base64Encode(string: string): string {
    if (typeof btoa !== 'undefined') {
        // eslint-disable-next-line no-undef
        return btoa(string);
    }

    return Buffer.from(string).toString('base64');
}

export default class Api extends ApiBase<ApiResponseType<any>> {
    protected clientId: string;

    protected secret: string;

    protected accessToken?: string;

    public constructor(clientId: string, secret: string) {
        super('https://www.strava.com', [new DefaultResponseProcessor(DefaultApiException)], {
            'content-Type': 'application/json',
        });
        this.clientId = clientId;
        this.secret = secret;
    }

    public setAccessToken(token: string) {
        this.accessToken = token;
        this.setDefaultHeader('Authorization', `Bearer ${token}`);
    }

    public getAccessToken() {
        return this.accessToken;
    }

    // eslint-disable-next-line default-param-last
    public getLoginUrl(redirectUri: string, scope: Scope[] = ['read'], approvalPrompt?: Prompt, state?: string): string {
        const parameters = {
            client_id: this.clientId,
            redirect_uri: redirectUri,
            response_type: 'code',
            scope: scope.join(','),
            ...(state ? { state } : {}),
            ...(approvalPrompt ? { approval_prompt: approvalPrompt } : {}),
        };
        return `https://www.strava.com/oauth/authorize${ApiBase.convertParametersToUrl(parameters)}`;
    }

    protected async requestToken(parameters: Record<string, string>): Promise<Token> {
        const { data } = await this.request(
            `oauth/token${ApiBase.convertParametersToUrl({
                client_id: this.clientId,
                client_secret: this.secret,
                ...parameters,
            })}`,
            'POST',
            {},
            {
                'content-type': 'application/x-www-form-urlencoded',
                'Authorization': `Basic ${base64Encode(`${this.clientId}:${this.secret}`)}`,
            },
        );

        this.setAccessToken(data.access_token);

        return data;
    }

    public requestAccessToken(code: string): Promise<Token> {
        return this.requestToken({ code });
    }

    public refreshToken(token: string): Promise<Token> {
        return this.requestToken({ grant_type: 'refresh_token', refresh_token: token });
    }

    public async getActivity(id: number) {
        const { data } = await this.get(`api/v3/activities/${id}`);
        return Activity.getFromApi(data);
    }

    public async getStream(id: number, streams: STREAM.Stream[] = []): Promise<any[]> {
        const { data } = await this.get(`api/v3/activities/${id}/streams`, {
            keys: streams.join(','),
            key_by_type: false,
        });

        const points: any[] = [];
        Object.keys(data).forEach((item: any) => {
            data[item].data.forEach((value: any, key: any) => {
                if (!points[key]) {
                    points[key] = {};
                }
                points[key][item] = value;
            });
        });
        return points;
    }

    public async getActivities(parameters: ActivityFilters) {
        const { after, before } = parameters;
        const { data } = await this.get(
            `api/v3/athlete/activities${ApiBase.convertParametersToUrl({
                ...parameters,
                ...(after ? { after: typeof after === 'number' ? after : after.valueOf() / 1000 } : {}),
                ...(before ? { before: typeof before === 'number' ? before : before.valueOf() / 1000 } : {}),
            })}`,
        );

        return data.map((activity: ApiActivity) => {
            return Activity.getFromApi(activity);
        });
    }

    public async processActivities(
        // eslint-disable-next-line default-param-last
        filter: ActivityFilters = {},
        processor: (workout: Activity<number, ApiActivity>) => Promise<Activity<number, ApiActivity>>,
    ): Promise<Activity<number, ApiActivity>[]> {
        const activities = await this.getActivities(filter);

        const processorPromises = activities.map((activity: Activity<number, ApiActivity>) => {
            return processor(activity);
        });

        if (activities.length > 0) {
            const { page } = filter;
            processorPromises.push(
                ...(await this.processActivities(
                    {
                        ...filter,
                        page: page ? page + 1 : 2,
                    },
                    processor,
                )),
            );
        }

        return Promise.all(processorPromises);
    }

    // eslint-disable-next-line complexity
    public async uploadActivity(
        activity: Activity,
        fileContent: string | Buffer,
        externalId: string | number,
        dataType = 'gpx',
    ): Promise<UploadStatus> {
        const body = Api.convertData(
            {
                data_type: dataType,
                ...(activity.getTitle() != null ? { name: activity.getTitle() } : {}),
                ...(activity.getDescription() != null ? { description: activity.getDescription() } : {}),
                ...(activity.isCommute != null ? { commute: activity.isCommute ? 1 : 0 } : {}),
            },
            Api.FORMATS.FORM_DATA,
        ) as any as FormData;

        body.append('file', fileContent, {
            filename: `${externalId}.${dataType}`,
        });

        const contentType = this.getDefaultHeaders()['content-Type'] as string;
        this.removeDefaultHeader('content-Type');

        const { data } = await this.request('api/v3/uploads', 'POST', { body: body as any });

        this.setDefaultHeader('content-Type', contentType);

        return data;
    }

    public async getUploadStatus(uploadId: number): Promise<UploadStatus> {
        const { data } = await this.get(`api/v3/uploads/${uploadId}`);

        if (data.error) {
            throw new StravaException(data.error);
        }

        return data;
    }

    public async createActivity(activity: Activity<undefined, undefined>): Promise<Activity<number, ApiActivity>> {
        const { data } = await this.post('api/v3/activities', activity.toApiObject());
        return Activity.getFromApi(data);
    }

    public async updateActivity(activity: Activity<number>): Promise<Activity> {
        const { data } = await this.put(`api/v3/activities/${activity.getId()}`, activity.toApiObject());
        return Activity.getFromApi(data);
    }

    public async updateWeight(weight: number): Promise<Athlete> {
        const { data } = await this.put('api/v3/athlete', { weight });
        return data;
    }
}
