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>;
}

export interface ForgeHostClient {
    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;
  descriptorExists: boolean;

  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
   * @returns HostClient a httpClient
   * @deprecated Connect EoS has been announced, replace with Forge-aware clients
   */
  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
   * @deprecated Connect EoS has been announced, replace with Forge-aware clients
   */
  httpClient(
    reqOrOpts: express.Request | { clientKey: string; userAccountId?: string }
  ): HostClient;

  /**
   * Intended to be used in routes secured by `authenticateForge()`.
   * @param req an expressRequest object
   * @returns ForgeHostClient a Forge-aware HTTP client that injects the Forge USER token from the invocation context.
   * @throws error when used in route not secured by `authenticateForge()`.
   */
  forgeHttpClientAsUser(req: express.Request): ForgeHostClient;

  /**
   * Intended to be used in routes secured by `authenticateForge()`.
   * @param req an expressRequest object.
   * @returns ForgeHostClient a Forge-aware HTTP client that injects the Forge APP token from the invocation context.
   * @throws error when used in route not secured by `authenticateForge()`.
   */
  forgeHttpClientAsApp(req: express.Request): ForgeHostClient;

  /**
   * Intended to be used in asynchronous invocations where the asApp token is cached.
   * @param installationId the Forge installation ID that the request should be made for.
   * @returns Promise containing ForgeHostClient, a Forge-aware HTTP client that uses the Forge App token from cache.
   * @throws error when authentication data not cached for given installation ID.
   */
  forgeHttpClientAsAppForInstallation(
    installationId: string
  ): Promise<ForgeHostClient>;
  /* 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;
