import { type Logger } from "@foxxmd/logging";
import request, { Request } from "superagent";
import { truncateStringToLength } from "../../core/StringUtils.ts";
import { isSuperAgentResponseError } from "../common/errors/ErrorUtils.ts";
import { isNodeNetworkException } from "../common/errors/NodeErrors.ts";
import { UpstreamError } from "../common/errors/UpstreamError.ts";
import {
    type AppriseConfig,
    type PrioritiesConfig,
    type Priority,
    type WebhookPayload
} from "../common/infrastructure/config/health/webhooks.ts";
import { AbstractWebhookNotifier } from "./AbstractWebhookNotifier.ts";
import { type URLData } from "../../core/Atomic.ts";
import { isPortReachable, joinedUrl, normalizeWebAddress } from "../utils/NetworkUtils.ts";
import { isDebugMode } from "../utils.ts";

const shortKey = truncateStringToLength(10);

export class AppriseWebhookNotifier extends AbstractWebhookNotifier {

    declare config: AppriseConfig;

    priorities: PrioritiesConfig;

    protected endpoint: URLData;

    urls: string[];
    keys: string[];
    tags: string[];

    constructor(defaultName: string, config: AppriseConfig, logger: Logger) {
        super('Apprise', defaultName, config, logger);
        const {
            urls = [],
            keys = [],
            tags = [],
            host,
        } = this.config;
        if (host === undefined) {
            throw new Error(`'host' must be defined in configuration for this notification`);
        }
        this.urls = Array.isArray(urls) ? urls : [urls];
        this.keys = Array.isArray(keys) ? keys : [keys];
        this.tags = Array.isArray(tags) ? tags : [tags];

        if (this.urls.length === 0 && this.keys.length === 0) {
            this.logger.warn(`No 'urls' or 'keys' were defined! Will assume stateless (POST ${host}/notify) and that you have the ENV 'APPRISE_STATELESS_URLS' set on your Apprise instance`);
        }
    }

    initialize = async () => {
        // check url is correct as a courtesy
        this.endpoint = normalizeWebAddress(this.config.host);
        this.logger.verbose(`Config URL: '${this.config.host}' => Normalized: '${this.endpoint.normal}'`)

        this.initialized = true; // always set as ready to go. Server issues may be transient.

        try {
            await isPortReachable(this.endpoint.port, { host: this.endpoint.url.hostname });
        } catch (e) {
            this.logger.warn(new Error('Unable to detect if server is reachable', { cause: e }));
            return;
        }

        if (this.keys.length > 0) {
            let anyOk = false;
            for (const key of this.keys) {
                try {
                    const resp = await request.get(joinedUrl(this.endpoint.url, `/json/urls/${key}`).toString());
                    if (resp.statusCode === 204) {
                        this.logger.warn(`Details for Config ${shortKey(key)} returned no content. Double check the key is set correctly or that the apprise Config is not empty.`);
                    } else {
                        anyOk = true;
                    }
                } catch (e) {
                    this.logger.warn(new Error(`Failed to get details for Config ${shortKey(key)}`, {cause: e}));
                }
            }
            if (!anyOk) {
                this.logger.warn('No Apprise Configs were valid!');
            }
        }
        this.initialized = true;
    }

    doNotify = async (payload: WebhookPayload) => {
        const body: Record<string, any> = {
            title: payload.title !== undefined ? `${payload.identifier} - ${payload.title}` : undefined,
            body: payload.message,
            tags: this.tags.join(','), // a comma 'OR's the tags https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-stateful-storage-solution
            type: convertPriorityToType(payload.priority)
        }

        let anyOk = false;
        if (this.keys.length > 0) {
            for (const key of this.keys) {
                try {
                    const resp = await this.callApi(request.post(joinedUrl(this.endpoint.url, `/notify/${key}`).toString())
                        .type('json')
                        .send(body));
                    anyOk = true;
                    this.logger.verbose(`Pushed notification to Config ${shortKey(key)}`);
                } catch (e: any) {
                    this.logger.warn(new Error(`Failed to push notification for '${payload.title}' to Config ${shortKey(key)}`, {cause: e}));
                    if(isDebugMode()) {
                        this.logger.debug({ body }, 'Apprise Request')
                    }
                }
            }
        }

        if (this.urls.length > 0 || this.keys.length === 0) {
            if (this.urls.length > 0) {
                body.urls = this.urls.join(',')
            }
            try {
                const resp = await this.callApi(request.post(joinedUrl(this.endpoint.url, '/notify').toString())
                    .type('json')
                    .send(body));
                anyOk = true;
                this.logger.verbose(`Pushed notification to URLs`);
            } catch (e: any) {
                this.logger.warn(new Error(`Failed to push notification for '${payload.title}' to URLs`, {cause: e}));
                if(isDebugMode()) {
                    this.logger.debug({ body }, 'Apprise Request')
                }
            }
        }

        if (!anyOk) {
            this.logger.error(`Failed to push any notifications!`)
        }
    }

    callApi = async <T = unknown>(req: Request, retries = 0): Promise<T> => {
        try {
            return await req as T;
        } catch (e) {
            if (isNodeNetworkException(e) || isSuperAgentResponseError(e) && e.timeout) {
                throw new UpstreamError('Request failed to due a network issue', {cause: e});
            } else if (isSuperAgentResponseError(e)) {
                const {
                    message,
                    status,
                    response: {
                        body: jsonBody = undefined,
                        text = undefined,
                    } = {}
                } = e;
                const errorMsgs = [message];
                if(typeof jsonBody === 'object') {
                    let appriseError = 'Apprise Error Response';
                    if ('error' in jsonBody) {
                        appriseError = `${appriseError}: ${jsonBody.error}`;
                    }
                    if('details' in jsonBody) {
                        const appriseDetails = [];
                        if(Array.isArray(jsonBody.details)) {
                            for(const detailEntry of jsonBody.details) {
                                try {
                                appriseDetails.push(`(${detailEntry[0]}) ${detailEntry[2]}`);
                                } catch (e) {
                                    appriseDetails.push(JSON.stringify(detailEntry));
                                }
                            }
                        } else {
                            appriseDetails.push(JSON.stringify(jsonBody.details));
                        }
                        appriseError = `${appriseError} --> ${appriseDetails.join(' || ')} <--`;
                    }
                    errorMsgs.push(appriseError);
                }
                throw new UpstreamError(`Apprise API Request failed => (${status}) ${errorMsgs.join(' => ')}`, {response: e.response});
            } else {
                throw new Error('Non API Request error encountered', {cause: e});
            }
        }
    }
}

const convertPriorityToType = (priority?: Priority): 'info' | 'success' | 'warning' | 'failure' => {
    switch (priority) {
        case 'info':
            return 'info';
        case 'warn':
            return 'warning';
        case 'error':
            return 'failure';
        default:
            return 'info';
    }
}
