import { Account } from "lincd-ads/lib/shapes/Account";
import { Ad } from "lincd-ads/lib/shapes/Ad";
import { AdSet } from "lincd-ads/lib/shapes/AdSet";
import { Campaign } from "lincd-ads/lib/shapes/Campaign";
import { restAPI } from "lincd-rest-api/lib/ontologies/restapi";
import { API } from "lincd-rest-api/lib/shapes/API";
import { mergeOptions } from "lincd-rest-api/lib/utils";
import { Literal, NamedNode } from "lincd/lib/models";
import { literalProperty } from "lincd/lib/utils/ShapeDecorators";
import {
    AccountResponse,
    DataResponse,
    QuoraOptions,
    ResolvedQuoraResponse,
    field,
} from "../interfaces";
import AccountMapping from "../mappings/Account";
import AdMapping from "../mappings/Ad";
import AdSetMapping from "../mappings/AdSet";
import CampaignMapping from "../mappings/Campaign";
import { qads } from "../ontologies/qads";
import { linkedShape } from "../package";

// Just being used to reflect the default fields that are used
// in the docs, not necessary to include
const DEFAULT_FIELDS: readonly field[] = [
    "accountId",
    "clicks",
    "impressions",
    "spend",
] as const;

@linkedShape
export class QuoraAPI extends API {
    /**
     * indicates that instances of this shape need to have this rdf.type
     */
    static targetClass: NamedNode = qads.QuoraAPI;

    processResponse: (r: Response) => void = (r) => {
        const remaining = r.headers.get("X-Rate-Limit-Remaining");

        console.info(`⏳ ${remaining} request(s) remaining...`);
    };

    constructor(node?);
    constructor(accessToken: string, refreshToken: string);
    constructor(
        nodeOrAccessToken: any,
        refreshToken?: string,
        clientId: string = "",
        clientSecret: string = ""
    ) {
        super();
        if (typeof nodeOrAccessToken === "string") {
            let accessToken = nodeOrAccessToken;

            // Headers as per Quora's documentation;
            // https://t.ly/z47ub
            this.defaultHeaders = {
                Authorization: `Bearer ${accessToken}`,
            };

            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
            this.clientId = clientId;
            this.clientSecret = clientSecret;
        } else super(nodeOrAccessToken);
        this.host = "https://api.quora.com/ads/v0";
    }

    /**
     * Helper method for generating URL-ready parameters from
     * given options.
     *
     * This is unique to Quora, and may vary from API to API.
     * To make this, I looked through the docs to see exactly
     * what params can be used in the docs: https://t.ly/DJveM
     *
     * @param options
     * @returns A URL-ready string with the provided `options`
     */
    generateParamsFromOptions(options?: QuoraOptions): string {
        if (!options) {
            return "";
        }

        let params: string[] = [];

        for (var key in options) {
            let param = key;
            let val = options[key];

            let ignore = false;

            switch (key) {
                case "attributionWindows":
                case "conversionTypes":
                case "fields":
                    param += `=${val.join(",")}`;
                    break;
                case "endDate":
                case "startDate":
                    let date: Date = val;
                    param += `=${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
                    break;
                case "granularity":
                case "level":
                case "offset":
                case "order":
                case "presetTimeRange":
                case "sort":
                case "sortConversionType":
                    param += `=${val}`;
                    break;
                case "summary":
                    // Since `summary` is a boolean,
                    // check if they set it to true ...
                    if (val) {
                        break;
                    }
                    // ... otherwise, don't add the param
                    // at all:
                    // https://www.quora.com/ads/api9169a6d6e9b42452d500a61717d87d15d5fa49ec5b53030741178130?share=1#/paths/~1campaigns~1{campaign-id}/get
                    param = "";
                    break;
                case "limit":
                    ignore = true;
                    break;
                default:
                    console.warn(`Unknown key: '${key}' with value '${val}'`);
                    break;
            }

            if (!ignore) {
                params.push(param);
            }
        }

        return `?${params.join("&")}`;
    }

    /**
     * Fetches an ad belonging to the authorised client.
     * @param adId
     * @param options
     * @returns
     */
    async getAd(adId: number, options: QuoraOptions = {}): Promise<Ad> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const adData: DataResponse = res.data[0];

            const ad = AdMapping.initialise(adData);

            return ad;
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: ["adId"],
        };

        options = {
            fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)],
        };

        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        return (await this.makeRequest(
            `/ads/${adId}${params}`,
            mappingFn
        )) as unknown as Ad;
    }

    /**
     * Retrieve a single ad set by its ID.
     * @param adSetId The ID belonging to the ad set to fetch
     * @param options Any extra options to add to the search
     * @returns A shape of the request ad
     */
    async getAdSet(
        adSetId: number,
        options: QuoraOptions = {}
    ): Promise<AdSet> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const adSetData: DataResponse = res.data[0];

            const adSet = AdSetMapping.initialise(adSetData);

            return adSet;
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: ["adSetId"],
        };

        options = {
            fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)],
        };

        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        return (await this.makeRequest(
            `/ad-sets/${adSetId}${params}`,
            mappingFn
        )) as unknown as AdSet;
    }

    /**
     * Retrieve a single campaign by its ID
     * @param campaignId The ID by which to search for the data
     * @param options Any extra options to search by
     * @returns The populated campaign shape
     */
    async getCampaign(
        campaignId: number,
        options: QuoraOptions = {}
    ): Promise<Campaign> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const campaignData: DataResponse = res.data[0];

            const campaign = CampaignMapping.initialise(campaignData);

            return campaign;
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: ["campaignId"],
        };

        options = {
            fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)],
        };

        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        return this.makeRequest(
            `/campaigns/${campaignId}${params}`,
            mappingFn
        ) as unknown as Campaign;
    }

    async getAdSetsFromCampaign(
        campaignId: number,
        options: QuoraOptions = {}
    ): Promise<AdSet[]> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const adSetList: DataResponse[] = res.data;

            return adSetList?.map((adSetJSON) => {
                const adSet = AdSetMapping.initialise(adSetJSON);

                return adSet;
            });
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: [
                "adSetId",
                "campaignId",
                ...(!options.fields ? DEFAULT_FIELDS : []),
            ],
            level: "AD_SET",
        };
        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        const limit = options?.limit || 0;

        return (await this.paginatedRequest(
            `/campaigns/${campaignId}${params}`,
            mappingFn,
            "paging",
            limit
        )) as AdSet[];
    }

    /**
     * Default options for this method are ``{
     *      fields: ["adId"]
     *      level: "AD"
     * }`` as these are the bare minimum required for the endpoint to
     * return meaningful data and construct shapes.
     *
     * @param adSetId The ID of the Ad Set from which to collect ads
     * @param options Any additional options to be applied to the defaults
     * @returns An array of Ad shape instances
     */
    async getAdsFromAdSet(
        adSetId: number,
        options: QuoraOptions = {}
    ): Promise<Ad[]> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const adList: DataResponse[] = res.data;

            return adList?.map((adJSON) => {
                const ad = AdMapping.initialise(adJSON);

                return ad;
            });
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: [
                "adId",
                "adSetId",
                ...(!options.fields ? DEFAULT_FIELDS : []),
            ],
            level: "AD",
        };
        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        const limit = options?.limit || 0;

        return (await this.paginatedRequest(
            `/ad-sets/${adSetId}${params}`,
            mappingFn,
            "paging",
            limit
        )) as Ad[];
    }

    /**
     * Default options for this method are ``{
     *      fields: ["adId"]
     *      level: "AD"
     * }`` as these are the bare minimum required for the endpoint to
     * return meaningful data and construct shapes.
     *
     * @param campaignId The ID of the campaign from which to collect ads
     * @param options Any additional options to be applied to the defaults
     * @returns An array of Ad shape instances
     */
    async getAdsFromCampaign(
        campaignId: number,
        options: QuoraOptions = {}
    ): Promise<Ad[]> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const adList = res.data;

            return adList?.map((adJSON: DataResponse) => {
                const ad = AdMapping.initialise(adJSON);

                return ad;
            });
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: ["adId", ...(!options.fields ? DEFAULT_FIELDS : [])],
            level: "AD",
        };
        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        const limit = options?.limit || 0;

        return (await this.paginatedRequest(
            `/campaigns/${campaignId}${params}`,
            mappingFn,
            "paging",
            limit
        )) as Ad[];
    }

    async getAllAccounts(): Promise<Account[]> {
        const mappingFn = (res: AccountResponse) => {
            const accounts = res.data;

            return accounts?.map((accData) => {
                const account: Account = AccountMapping.initialise(accData);

                return account;
            });
        };

        return (await this.makeRequest(
            "/accounts/",
            mappingFn
        )) as unknown as Account[]; // TODO fix so don't need as unknown
    }

    async getCampaignsFromAccount(
        accountId: number,
        options: QuoraOptions = {},
        returnOriginalJSON: boolean = false
    ): Promise<Campaign[] | [Campaign[], ResolvedQuoraResponse]> {
        const mappingFn = (res: ResolvedQuoraResponse) => {
            const campaignData: DataResponse[] = res.data;
            const campaignList = campaignData?.map((campaignJSON) => {
                const campaign = CampaignMapping.initialise(campaignJSON);

                return campaign;
            });

            // Should this be an option at the lincd-rest-api
            // level?  Or should it be up to the API designer?
            if (returnOriginalJSON) {
                // Just seems like a bit of a band-aid solution
                return [campaignList, res];
            }

            return campaignList;
        };

        const DEFAULT_OPTIONS: QuoraOptions = {
            fields: ["campaignId", ...(!options.fields ? DEFAULT_FIELDS : [])],
            level: "CAMPAIGN",
        };
        options = mergeOptions(DEFAULT_OPTIONS, options);

        const params = this.generateParamsFromOptions(options);

        const limit = options?.limit || 0;

        return (await this.paginatedRequest(
            `/accounts/${accountId}${params}`,
            mappingFn,
            "paging",
            limit
        )) as Campaign[];
    }

    async refreshAccessToken() {
        if (
            typeof this.clientId === "undefined" ||
            typeof this.clientSecret === "undefined"
        ) {
            throw new Error("Client ID or Secret not provided");
        }

        const body = [
            `client_id=${this.clientId}`,
            `client_secret=${this.clientSecret}`,
            `refresh_token=${this.refreshToken}`,
            "grant_type=refresh_token",
        ].join("&");

        let res = await fetch("https://www.quora.com/_/oauth/token", {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-url-encoded",
            },
            body,
        });
        let json = await res.json();

        this.accessToken = json.access_token;
        this.defaultHeaders = {
            ...this.defaultHeaders,
            Authorization: `Bearer ${this.accessToken}`,
        };
    }

    @literalProperty({
        path: restAPI.accessToken,
        required: true,
        maxCount: 1,
        minLength: 30,
        maxLength: 30,
    })
    get accessToken() {
        return this.getValue(restAPI.accessToken);
    }

    set accessToken(val: string) {
        this.overwrite(restAPI.accessToken, new Literal(val));
    }

    @literalProperty({
        path: restAPI.clientId,
        required: true,
        maxCount: 1,
        minLength: 32,
        maxLength: 32,
    })
    get clientId() {
        return this.getValue(restAPI.clientId);
    }

    set clientId(val: string) {
        this.overwrite(restAPI.clientId, new Literal(val));
    }

    @literalProperty({
        path: restAPI.clientSecret,
        required: true,
        maxCount: 1,
        minLength: 44,
        maxLength: 44,
    })
    get clientSecret() {
        return this.getValue(restAPI.clientSecret);
    }

    set clientSecret(val: string) {
        this.overwrite(restAPI.clientSecret, new Literal(val));
    }

    @literalProperty({
        path: restAPI.refreshToken,
        required: true,
        maxCount: 1,
        minLength: 30,
        maxLength: 30,
    })
    get refreshToken() {
        return this.getValue(restAPI.refreshToken);
    }

    set refreshToken(val: string) {
        this.overwrite(restAPI.refreshToken, new Literal(val));
    }
}
