import { Router } from 'express';
import * as API from '@unito/integration-api';
import { InvalidHandler } from './errors.js';
import { HttpError, UnauthorizedError, BadRequestError, UnprocessableEntityError } from './httpErrors.js';
import {
  GetBlobContext,
  GetItemContext,
  GetCollectionContext,
  CreateBlobContext,
  CreateItemContext,
  UpdateItemContext,
  DeleteItemContext,
  GetCredentialAccountContext,
  ParseWebhooksContext,
  UpdateWebhookSubscriptionsContext,
  AcknowledgeWebhooksContext,
  ParseRichTextContext,
  DumpRichTextContext,
} from './resources/context.js';

import busboy, { FileInfo } from 'busboy';
/**
 * Handler called to get an individual item.
 *
 * @param context {@link GetItemContext}
 * @returns The requested {@link API.Item}.
 */
export type GetItemHandler = (context: GetItemContext<any, any>) => Promise<API.Item>;

/**
 * Handler called to retrieve a collection of items.
 *
 * @param context {@link GetCollectionContext}
 * @return An {@link API.Collection} containing requested items and a link to the next page, if applicable.
 */
export type GetCollectionHandler = (context: GetCollectionContext<any, any>) => Promise<API.Collection>;

/**
 * Handler called to create an item.
 *
 * @param context {@link CreateBlobContext}
 * @returns An {@link API.ItemSummary} containing a path to the created item.
 */
export type CreateBlobHandler = (context: CreateBlobContext<any, any>) => Promise<API.ItemSummary>;

/**
 * Handler called to create an item.
 *
 * @param context {@link CreateItemContext}
 * @returns An {@link API.ItemSummary} containing a path to the created item.
 */
export type CreateItemHandler = (context: CreateItemContext<any, any, any>) => Promise<API.ItemSummary>;

/**
 * Handler called to update an item.
 *
 * @param context {@link UpdateItemContext}
 * @returns The updated {@link API.Item}.
 */
export type UpdateItemHandler = (context: UpdateItemContext<any, any, any>) => Promise<API.Item>;

/**
 * Handler called to delete an item.
 *
 * @param context {@link DeleteItemContext}
 */
export type DeleteItemHandler = (context: DeleteItemContext<any, any>) => Promise<void>;

/**
 * Handler called to get a Binary Large Object.
 *
 * @param context {@link BlobItemContext}
 * @returns A {@link ReadableStream} of the Blob.
 */
export type GetBlobHandler = (context: GetBlobContext<any, any>) => Promise<ReadableStream<Uint8Array>>;

/**
 * Handler called to retrieve the account details associated with the credentials.
 *
 * @param context {@link GetCredentialAccountContext}
 * @returns The {@link API.CredentialAccount} associated with the credentials.
 */
export type GetCredentialAccountHandler = (
  context: GetCredentialAccountContext<any, any>,
) => Promise<API.CredentialAccount>;

/**
 * Handler called to parse the content of an incoming webhook.
 *
 * @param context {@link ParseWebhooksContext}
 * @returns The parsed content of the webhook as a {@link API.WebhookParseResponsePayload}.
 */
export type ParseWebhooksHandler = (
  context: ParseWebhooksContext<any, any, any>,
) => Promise<API.WebhookParseResponsePayload>;

/**
 * Handler called to subscribe or unsubscribe to a particular webhook.
 *
 * @param context {@link UpdateWebhookSubscriptionsContext}
 */
export type UpdateWebhookSubscriptionsHandler = (
  context: UpdateWebhookSubscriptionsContext<any, any, any>,
) => Promise<void>;

/**
 * Handler called to acknowledge the reception of a webhook.
 *
 * @param context {@link AcknowledgeWebhooksContext}
 * @returns The {@link API.WebhookAcknowledgeResponsePayload} to be sent back to the webhook provider.
 */
export type AcknowledgeWebhooksHandler = (
  context: AcknowledgeWebhooksContext<any, any, any>,
) => Promise<API.WebhookAcknowledgeResponsePayload>;

/**
 * Handler called to parse a provider-native rich text value into the canonical AST.
 *
 * @param context {@link ParseRichTextContext}
 * @returns The parsed AST as a {@link API.RichTextParseResponsePayload}.
 */
export type ParseRichTextHandler = (
  context: ParseRichTextContext<any, any, any>,
) => Promise<API.RichTextParseResponsePayload>;

/**
 * Handler called to dump a canonical AST back to a provider-native rich text value.
 *
 * @param context {@link DumpRichTextContext}
 * @returns The provider-native value as a {@link API.RichTextDumpResponsePayload}.
 */
export type DumpRichTextHandler = (
  context: DumpRichTextContext<any, any, any>,
) => Promise<API.RichTextDumpResponsePayload>;

/**
 * Defines the implementation of the operations available for a given `Item`.
 * - In some cases (e.g. defining the `root` or {@link https://dev.unito.io/docs/connectors/apiSpecification/credentialAccount | me}
 *   handlers), only a {@link GetItemHandler | getItem} handler is necessary.
 * - In most cases, you will want to define {@link GetCollectionHandler | getCollection},
 *   most likely {@link GetItemHandler | getItem}, and any other handlers relevant to the item.
 *
 * The `ItemHandlers` object can contain any of the following:
 * - {@link GetItemHandler | getItem}: A handler called to get an individual item.
 * - {@link GetCollectionHandler | getCollection}: A handler called to retrieve a collection of items.
 * - {@link CreateItemHandler | createItem}: A handler called to create an item.
 * - {@link UpdateItemHandler | updateItem}: A handler called to update an item.
 * - {@link DeleteItemHandler | deleteItem}: A handler called to delete an item.
 */
export type ItemHandlers = {
  getItem?: GetItemHandler;
  getCollection?: GetCollectionHandler;
  createBlob?: CreateBlobHandler;
  createItem?: CreateItemHandler;
  updateItem?: UpdateItemHandler;
  deleteItem?: DeleteItemHandler;
};

export type BlobHandlers = {
  getBlob: GetBlobHandler;
  createBlob?: CreateBlobHandler;
};

export type CredentialAccountHandlers = {
  getCredentialAccount: GetCredentialAccountHandler;
};

export type ParseWebhookHandlers = {
  parseWebhooks: ParseWebhooksHandler;
};

export type WebhookSubscriptionHandlers = {
  updateWebhookSubscriptions: UpdateWebhookSubscriptionsHandler;
};

export type AcknowledgeWebhookHandlers = {
  acknowledgeWebhooks: AcknowledgeWebhooksHandler;
};

export type ParseRichTextHandlers = {
  parseRichText: ParseRichTextHandler;
};

export type DumpRichTextHandlers = {
  dumpRichText: DumpRichTextHandler;
};

export type HandlersInput =
  | ItemHandlers
  | BlobHandlers
  | CredentialAccountHandlers
  | ParseWebhookHandlers
  | WebhookSubscriptionHandlers
  | AcknowledgeWebhookHandlers
  | ParseRichTextHandlers
  | DumpRichTextHandlers;

type Handlers = Partial<
  ItemHandlers &
    BlobHandlers &
    CredentialAccountHandlers &
    ParseWebhookHandlers &
    WebhookSubscriptionHandlers &
    AcknowledgeWebhookHandlers &
    ParseRichTextHandlers &
    DumpRichTextHandlers
>;

type Path = string & { __brand: 'Path' };

function assertValidPath(path: string): asserts path is Path {
  if (!path.startsWith('/')) {
    throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
  }

  if (path.length > 1 && path.endsWith('/')) {
    throw new InvalidHandler(`The provided path '${path}' is invalid. Paths must not end with a '/'.`);
  }
}

function assertValidConfiguration(path: Path, pathWithIdentifier: Path, handlers: Handlers) {
  if (path === pathWithIdentifier) {
    const individualHandlers = ['getItem', 'updateItem', 'deleteItem'];
    const collectionHandlers = ['getCollection', 'createItem'];

    const hasIndividualHandlers = individualHandlers.some(handler => handler in handlers);
    const hasCollectionHandlers = collectionHandlers.some(handler => handler in handlers);

    if (hasIndividualHandlers && hasCollectionHandlers) {
      throw new InvalidHandler(
        `The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both.`,
      );
    }
  }
}

function parsePath(path: Path) {
  const pathParts = path.split('/');

  const lastPart = pathParts.at(-1);

  if (pathParts.length > 1 && lastPart && lastPart.startsWith(':')) {
    pathParts.pop();
  }

  return { pathWithIdentifier: path, path: pathParts.join('/') as Path };
}

function assertCreateItemRequestPayload(body: unknown): asserts body is API.CreateItemRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid CreateItemRequestPayload');
  }
  if (Object.keys(body).length === 0) {
    throw new UnprocessableEntityError('Empty CreateItemRequestPayload');
  }
}

function assertUpdateItemRequestPayload(body: unknown): asserts body is API.UpdateItemRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid UpdateItemRequestPayload');
  }
  if (Object.keys(body).length === 0) {
    throw new UnprocessableEntityError('Empty UpdateItemRequestPayload');
  }
}

function assertWebhookParseRequestPayload(body: unknown): asserts body is API.WebhookParseRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid WebhookParseRequestPayload');
  }

  if (!('payload' in body) || typeof body.payload !== 'string') {
    throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
  }

  if (!('url' in body) || typeof body.url !== 'string') {
    throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
  }
}

function assertRichTextParseRequestPayload(body: unknown): asserts body is API.RichTextParseRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid RichTextParseRequestPayload');
  }

  if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
    throw new BadRequestError("Missing required 'itemPath' property in RichTextParseRequestPayload");
  }

  if (!('relationName' in body) || typeof body.relationName !== 'string') {
    throw new BadRequestError("Missing required 'relationName' property in RichTextParseRequestPayload");
  }

  if (!('fieldName' in body) || typeof body.fieldName !== 'string') {
    throw new BadRequestError("Missing required 'fieldName' property in RichTextParseRequestPayload");
  }

  if (!('value' in body) || typeof body.value !== 'string') {
    throw new BadRequestError("Missing required 'value' property in RichTextParseRequestPayload");
  }
}

function assertRichTextDumpRequestPayload(body: unknown): asserts body is API.RichTextDumpRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid RichTextDumpRequestPayload');
  }

  if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
    throw new BadRequestError("Missing required 'itemPath' property in RichTextDumpRequestPayload");
  }

  if (!('relationName' in body) || typeof body.relationName !== 'string') {
    throw new BadRequestError("Missing required 'relationName' property in RichTextDumpRequestPayload");
  }

  if (!('fieldName' in body) || typeof body.fieldName !== 'string') {
    throw new BadRequestError("Missing required 'fieldName' property in RichTextDumpRequestPayload");
  }

  if (!('root' in body) || typeof body.root !== 'object' || body.root === null) {
    throw new BadRequestError("Missing required 'root' property in RichTextDumpRequestPayload");
  }
}

function assertWebhookSubscriptionRequestPayload(body: unknown): asserts body is API.WebhookSubscriptionRequestPayload {
  if (typeof body !== 'object' || body === null) {
    throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
  }

  if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
    throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
  }

  if (!('targetUrl' in body) || typeof body.targetUrl !== 'string') {
    throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
  }

  if (!('action' in body) || typeof body.action !== 'string') {
    throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
  }

  if (!['start', 'stop'].includes(body.action)) {
    throw new BadRequestError("Invalid value for 'action' property in WebhookSubscriptionRequestPayload");
  }
}

export class Handler {
  private path: Path;
  private pathWithIdentifier: Path;

  private handlers: Handlers;

  constructor(inputPath: string, handlers: HandlersInput) {
    assertValidPath(inputPath);

    const { pathWithIdentifier, path } = parsePath(inputPath);

    assertValidConfiguration(path, pathWithIdentifier, handlers);

    this.pathWithIdentifier = pathWithIdentifier;
    this.path = path;

    this.handlers = handlers;
  }

  public generate(): Router {
    const router: Router = Router({ caseSensitive: true });

    console.debug(`\x1b[33mMounting handler at path ${this.pathWithIdentifier}`);

    if (this.handlers.getCollection) {
      const handler = this.handlers.getCollection;

      console.debug(`  Enabling getCollection at GET ${this.path}`);

      router.get(this.path, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        const collection = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          search: res.locals.search,
          selects: res.locals.selects,
          filters: res.locals.filters,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
          relations: res.locals.relations,
        });

        res.status(200).send(collection);
      });
    }

    if (this.handlers.createItem) {
      const handler = this.handlers.createItem;

      console.debug(`  Enabling createItem at POST ${this.path}`);

      router.post(this.path, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        assertCreateItemRequestPayload(req.body);

        res.locals.logger.decorate({ fieldNames: Object.keys(req.body) });

        const createItemSummary = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          body: req.body,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(201).send(createItemSummary);
      });
    }

    if (this.handlers.createBlob) {
      const handler = this.handlers.createBlob;

      console.debug(`  Enabling createBlob at POST ${this.pathWithIdentifier}`);

      router.post(this.path, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }
        /**
         * Some of the integrations, servicenow for example,
         * will need to add more information to the form data that is being passed to the upload attachment handler.
         * This is why we need to use busboy to parse the form data, extract the information about the file and pass it to the handler.
         */
        const bb = busboy({ headers: req.headers });
        const formFields: Record<string, string> = {};

        bb.on('field', (name, value) => {
          formFields[name] = value;
        });

        bb.on('file', async (_name, file, info: FileInfo) => {
          try {
            const body: API.CreateBlobRequestPayload = {
              file: file,
              mimeType: info.mimeType,
              encoding: info.encoding,
              filename: info.filename,
            };

            if (formFields.fileSize) {
              body.fileSize = Number(formFields.fileSize);
            }

            const createdBlob = await handler({
              credentials: res.locals.credentials,
              secrets: res.locals.secrets,
              body,
              logger: res.locals.logger,
              deadline: res.locals.deadline,
              requestMetrics: res.locals.requestMetrics,
              params: req.params,
              query: req.query,
            });
            res.status(201).send(createdBlob);
          } catch (error) {
            if (error instanceof HttpError) {
              res.status(error.status).send(error);
            } else {
              res.status(500).send({ message: `Error creating the blob: ${error}` });
            }
          }
        });
        bb.on('error', error => {
          res.status(500).send({ message: `Error parsing the form data: ${error}` });
        });
        req.pipe(bb);
      });
    }

    if (this.handlers.getItem) {
      const handler = this.handlers.getItem;

      console.debug(`  Enabling getItem at GET ${this.pathWithIdentifier}`);

      router.get(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        const item = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(200).send(item);
      });
    }

    if (this.handlers.updateItem) {
      const handler = this.handlers.updateItem;

      console.debug(`  Enabling updateItem at PATCH ${this.pathWithIdentifier}`);

      router.patch(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        assertUpdateItemRequestPayload(req.body);

        res.locals.logger.decorate({
          fieldNames: Object.keys(req.body).filter(k => k !== '__meta'),
        });

        const item = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          body: req.body,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(200).send(item);
      });
    }

    if (this.handlers.deleteItem) {
      const handler = this.handlers.deleteItem;

      console.debug(`  Enabling deleteItem at DELETE ${this.pathWithIdentifier}`);

      router.delete(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(204).send(null);
      });
    }

    if (this.handlers.getBlob) {
      console.debug(`  Enabling getBlob at GET ${this.pathWithIdentifier}`);

      const handler = this.handlers.getBlob;

      router.get(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        const blob = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.writeHead(200, {
          'Content-Type': 'application/octet-stream',
        });

        const reader = blob.getReader();

        let isDone = false;

        try {
          while (!isDone) {
            const chunk = await reader.read();
            isDone = chunk.done;

            if (chunk.value) {
              res.write(chunk.value);
            }
          }
        } finally {
          reader.releaseLock();
        }

        res.end();
      });
    }

    if (this.handlers.getCredentialAccount) {
      const handler = this.handlers.getCredentialAccount;

      console.debug(`  Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);

      router.get(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        const credentialAccount = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(200).send(credentialAccount);
      });
    }

    if (this.handlers.acknowledgeWebhooks) {
      const handler = this.handlers.acknowledgeWebhooks;

      console.debug(`  Enabling acknowledgeWebhooks at POST ${this.pathWithIdentifier}`);

      router.post(this.pathWithIdentifier, async (req, res) => {
        assertWebhookParseRequestPayload(req.body);

        const response = await handler({
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
          body: req.body,
        });

        res.status(200).send(response);
      });
    }

    if (this.handlers.parseWebhooks) {
      const handler = this.handlers.parseWebhooks;

      console.debug(`  Enabling parseWebhooks at POST ${this.pathWithIdentifier}`);

      router.post(this.pathWithIdentifier, async (req, res) => {
        assertWebhookParseRequestPayload(req.body);

        const response = await handler({
          secrets: res.locals.secrets,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
          body: req.body,
        });

        res.status(200).send(response);
      });
    }

    if (this.handlers.updateWebhookSubscriptions) {
      const handler = this.handlers.updateWebhookSubscriptions;

      console.debug(`  Enabling updateWebhookSubscriptions at PUT ${this.pathWithIdentifier}`);

      router.put(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        assertWebhookSubscriptionRequestPayload(req.body);

        const response = await handler({
          secrets: res.locals.secrets,
          credentials: res.locals.credentials,
          body: req.body,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(204).send(response);
      });
    }

    if (this.handlers.parseRichText) {
      const handler = this.handlers.parseRichText;

      console.debug(`  Enabling parseRichText at POST ${this.pathWithIdentifier}`);

      router.post(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        assertRichTextParseRequestPayload(req.body);

        const response = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          body: req.body,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(200).send(response);
      });
    }

    if (this.handlers.dumpRichText) {
      const handler = this.handlers.dumpRichText;

      console.debug(`  Enabling dumpRichText at POST ${this.pathWithIdentifier}`);

      router.post(this.pathWithIdentifier, async (req, res) => {
        if (!res.locals.credentials) {
          throw new UnauthorizedError();
        }

        assertRichTextDumpRequestPayload(req.body);

        const response = await handler({
          credentials: res.locals.credentials,
          secrets: res.locals.secrets,
          body: req.body,
          logger: res.locals.logger,
          deadline: res.locals.deadline,
          requestMetrics: res.locals.requestMetrics,
          params: req.params,
          query: req.query,
        });

        res.status(200).send(response);
      });
    }

    console.debug(`\x1b[0m`);

    return router;
  }
}
