import * as events from "node:events";
import * as fs from "node:fs";
import * as querystring from "node:querystring";
import * as express from "express";
import * as request from "request";
import * as sequelize from "sequelize";

type Console = typeof console;

declare namespace AddOnFactory {
    // Actual exports of lib/index.js
export interface Descriptor {
    key: string;
    name: string;
    description: string;
    vendor: {
        name: string;
        url: string;
    };
    baseUrl: string;
    links: {
        self: string;
        homepage: string;
    };
    authentication: {
        type: string;
    };
    scopes: string[];
}

export interface Options {
    config: {
        descriptorTransformer?: (descriptor: Partial<Descriptor>, config: Config) => Descriptor
        development?: Partial<ConfigOptions>;
        production?: Partial<ConfigOptions>;
    }
}

export interface ConfigOptions {
    environment: string;
    port: string;
    store: {
        adapter?: string,
        type?: string,
        url?: string,
        storage?: string
    };
    expressErrorHandling: boolean;
    errorTemplate: boolean;
    errorTemplateName: string;
    errorTemplateObject:  Record<string, unknown>;
    validateDescriptor: boolean;
    localBaseUrl: string;
    jwt: {
        validityInMinutes: number;
    };
    product: string;
    hosts: string[];
    maxTokenAge: number;
    userAgent: string;
    watch: boolean;
}

export interface Config {
    port(): string
    environment(): string;
    store(): {
        adapter: string,
        type: string
    };
    expressErrorHandling(): boolean;
    errorTemplate(): boolean;
    getErrorTemplateName(): string;
    getErrorTemplateObject(): Record<string, unknown>;
    validateDescriptor(): boolean;
    localBaseUrl(): string;
    jwt(): {
        validityInMinutes: number;
    };
    product(): string;
    hosts(): string[];
    maxTokenAge(): number;
    userAgent(): string;
}

export interface KVStore {
    del(key: string): Promise<void>;
    get<T = unknown>(key: string): Promise<T>;
    set<T = unknown>(key: string, value: T | string): Promise<T>;
}

export interface StoreAdapter {
    del(key: string, clientKey: string): Promise<void>;
    get<T = unknown>(key: string, clientKey: string): Promise<T>;
    /**
     * Used to set data under `key` for a given client.
     * Do not use this to save clientInfo, use `saveInstallation` instead, as it handles associating Forge installations.
     */
    set<T = unknown>(key: string, value: T | string, clientKey: string): Promise<T>;
    getAllClientInfos(): Promise<ClientInfo[]>;
    saveInstallation(value: ClientInfo, clientKey: string): Promise<ClientInfo>;
    getClientSettingsForForgeInstallation(forgeInstallationId: string): Promise<ForgeInstallationClientSettings>;
    deleteAssociation(forgeInstallationId: string): Promise<void>;
    forForgeInstallation(installationId: string): KVStore;
}

export type ForgeInstallationClientSettings = {
    clientKey: string;
    installationId: string;
} | null;

export const DESCRIPTOR_FILENAME: "atlassian-connect.json";

export const store: {
    register(
        adapterKey: string,
        factory: (logger: Console, opts: unknown) => StoreAdapter
    ): void;
}

export type JiraPermissionsQuery = {
    project?: string[]
    global?: string[]
};

export type ConfluencePermissionsQuery = {
    application?: string[]
    content?: string
};

export type ModifyRequestArgsOptions = Omit<request.Options, 'form' | 'qs'> & {
    /** @deprecated Use multipartFormData instead */
    form?: request.Options['form'];
    multipartFormData?: Record<string, unknown | unknown[]>;
    urlEncodedFormData?: Record<string, unknown>;
    qs?: querystring.ParsedUrlQueryInput;
}|URL|string;

export type ModifyArgsOutput<
    TOptions extends ModifyRequestArgsOptions,
    TCallback extends request.RequestCallback
> = TCallback extends request.RequestCallback
    ? [TOptions, TCallback]
    : [TCallback];

export type HostClientArgs<
  TOptions extends ModifyRequestArgsOptions,
  TCallback extends request.RequestCallback
> = [
    TOptions, request.Headers, TCallback, string
];

export type BearerToken = {
    access_token: string;
    token_type: string;
    expires_in: number;
};

export type RequestOptions = ModifyRequestArgsOptions & {
    /** don't authenticate the request */
    anonymous?: boolean;
}

export type ForgeAppToken = {
    appToken: string;
    apiBaseUrl: string;
}

// This is a class that isn't exported, so we don't want to export a class that's not importable at runtime
// We declare it as an interface so consumers can do module augmentation though, instead of `declare class X` and exporting `InstanceType<typeof X>.
export interface HostClient {
    addon: AddOn;
    context: boolean;
    clientKey: string;
    oauth2: unknown;
    userKey?: string; // for impersonatingClient

    asUser(userKey: string): HostClient;
    asUserByAccountId: (userAccountId: string|number) => HostClient;
    createJwtPayload: (req: request.Request) => string;
    getUserBearerToken: (scopes: string[], clientSettings: ClientInfo) => Promise<BearerToken>;
    getBearerToken: (clientSettings: ClientInfo) => Promise<BearerToken>;

    defaults(): request.Request;
    cookie(): request.Cookie;
    jar(): request.CookieJar;

    modifyArgs<TOptions extends ModifyRequestArgsOptions = ModifyRequestArgsOptions, TCallback extends request.RequestCallback = request.RequestCallback>(...args: HostClientArgs<TOptions, TCallback>): ModifyArgsOutput<TOptions, TCallback>;

    get: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
    post: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
    put: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
    del: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
    head: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
    patch: <T = unknown, TOptions extends RequestOptions = RequestOptions>(options: TOptions, callback?: request.RequestCallback) => Promise<T>;
}

// This is a class that isn't exported, so we don't want to export a class that's not importable at runtime
// We declare it as an interface so consumers can do module augmentation though, instead of `declare class X` and exporting `InstanceType<typeof X>.
export interface AddOn extends events.EventEmitter {
    verifyInstallation(): express.RequestHandler;
    authenticateInstall(): express.RequestHandler;
    postInstallation(): (request: express.Request, response: express.Response) => void;
    middleware(): express.RequestHandler;
    authenticateForge(): express.RequestHandler;
    getForgeAppToken(): Promise<ForgeAppToken>;
    associateConnect(): express.RequestHandler;
    authenticate(skipQshVerification?: boolean): express.RequestHandler;
    authorizeJira(permissions: JiraPermissionsQuery): express.RequestHandler;
    authorizeConfluence(permissions: ConfluencePermissionsQuery): express.RequestHandler;
    loadClientInfo(clientKey: string): Promise<ClientInfo>;
    checkValidToken(): express.RequestHandler;

    register() : Promise<void>;
    key: string;
    name: string;
    config: Config;

    app: express.Application;

    deregister(): Promise<void>;

    descriptor: Descriptor;

    schema: sequelize.Sequelize;
    settings: StoreAdapter;

    shouldDeregister(): boolean;
    shouldRegister(): boolean;

    validateDescriptor(): {
        type: string;
        message: string;
        validationResults: {
            module: string;
            description: string;
            value?: unknown;
            validValues?: string[];
        }[]
    }[];

    watcher: fs.FSWatcher;

    /**
     * Reloads AddOn descriptor file
     */
    reloadDescriptor(): void;

    /* eslint-disable @typescript-eslint/unified-signatures */
    /**
     * @param opts a clientKey, and optionally a userKey
     * @deprecated to call with userKey, pass userAccountId instead.
    */
    httpClient(opts: { clientKey: string, userKey?: string }): HostClient;
    /**
     * @param reqOrOpts either an expressRequest object or an object containing a clientKey, and optionally a userAccountId
     * @returns HostClient a httpClient
     */
    httpClient(
        reqOrOpts: express.Request | { clientKey: string; userAccountId?: string }
    ): HostClient;
    /* eslint-enable @typescript-eslint/unified-signatures */
}

export interface FileNames {
    descriptorFilename?: string;
    configFileName?: string;
}


    export interface ClientInfo {
        key: string,
        clientKey: string,
        publicKey: string
        sharedSecret: string,
        serverVersion: string,
        pluginsVersion: string,
        baseUrl: string,
        displayUrl?: string;
        productType: string,
        description: string,
        eventType: string,
        oauthClientId?: string
    }
    export type AddOnFactory = typeof AddOnFactory;
} // End namespace AddOnFactory

// Declaring the function then a namespace is how to support the CJS export pattern we use in lib/index.js without using `export default` and requiring TS consumers to use `esModuleInterop: true`
// It also means that any types we want to expose to consumers have to be exported
// https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html#common-commonjs-patterns
// https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html

export = AddOnFactory;

// Overload definition to properly type how fileNames can be omitted.
declare function AddOnFactory(app: express.Application, opts?: AddOnFactory.Options, logger?: Console, callback?: () => void): AddOnFactory.AddOn;
declare function AddOnFactory(app: express.Application, opts?: AddOnFactory.Options, logger?: Console, fileNames?: AddOnFactory.FileNames, callback?: () => void): AddOnFactory.AddOn;
