import type { IncomingHttpHeaders } from 'http';
import { ServerResponse } from 'http';
import { isString, isObject } from '@teclone/utils';
import { FileServer } from './FileServer';
import type { Request } from './Request';
import { ErrorCallback, RServerConfig } from '../@types';
import type { Logger } from './Logger';
import { handleError } from './Utils';

export interface APIResponse<ResponseData = {}> {
  status?: 'success' | 'error';
  statusCode?: number;
  message?: string;
  data?: ResponseData;
  headers?: IncomingHttpHeaders;
  ttl?: number;
}

export interface APIExecutor<RequestBody, ResponseData> {
  (arg: { body: RequestBody; headers: IncomingHttpHeaders }): Promise<
    APIResponse<ResponseData> | null | undefined
  >;

  /**
   * assigned name of the handler
   */
  apiName?: string;
}
export class Response extends ServerResponse {
  config: RServerConfig = {} as RServerConfig;

  request: Request = {} as Request;

  logger: Logger = {} as Logger;

  errorCallback: ErrorCallback | null = null;

  startedAt: Date = {} as Date;

  endedAt: Date = {} as Date;

  constructor(req: Request) {
    super(req);
    this.request = req;
  }

  end(cb?: () => void): Promise<boolean>;

  end(data?: any, encoding?: string): Promise<boolean>;

  /**
   * ends the response with optional response data, and optional data encoding
   * @param data optional data to send. either string or buffer
   * @param encoding data encoding if not buffer
   */
  end(data, encoding?: BufferEncoding): Promise<boolean> {
    let resolvedData: string | Buffer = data;

    if (!Buffer.isBuffer(data) && !isString(data)) {
      if (isObject(data)) {
        resolvedData = JSON.stringify(data);
      } else {
        resolvedData = '';
      }
    }

    return new Promise((resolve) => {
      if (encoding) {
        super.end(resolvedData, encoding, () => {
          resolve(true);
        });
      } else {
        super.end(resolvedData, () => {
          resolve(true);
        });
      }
    });
  }

  /**
   * sets response header
   * @param name response header name
   * @param value response header value
   */
  setHeader(name: string, value: string | number | string[]): this {
    super.setHeader(name, value);
    return this;
  }

  /**
   * sets multiple response headers
   * @param headers object containing response header name value pairs
   */
  setHeaders(headers: { [p: string]: string | number | string[] }): this {
    Object.keys(headers).forEach((key) => {
      this.setHeader(key, headers[key]);
    });
    return this;
  }

  /**
   * removes a single set response header at a time. function is chainable
   * @param name response header to remove
   */
  removeHeader(name: string): this {
    super.removeHeader(name);
    return this;
  }

  /**
   * remove response headers that are already set. function is chainable
   * @param names - comma separated list of header names to remove
   */
  removeHeaders(...names: string[]): this {
    names.forEach((name) => {
      this.removeHeader(name);
    });
    return this;
  }

  /**
   * sets response status code
   * @param code - the response code
   */
  status(code: number): this {
    this.statusCode = code;
    return this;
  }

  /**
   * sends json response back to the client.
   * @param data - the json string or json object which will be stringified
   */
  json(data: object | string): Promise<boolean> {
    if (!isString(data)) {
      data = JSON.stringify(data);
    }
    return this.setHeader('Content-Type', 'application/json').end(data);
  }

  /**
   * Redirect client to the given url
   */
  redirect(path: string, status: number = 302): Promise<boolean> {
    return this.status(status).setHeader('Location', path).end();
  }

  /**
   * sends a file download attachment to the client
   * @param filePath - relative or absolute file path
   * @param filename - suggested file download name
   */
  download(filePath: string, filename?: string): Promise<boolean> {
    const fileServer = new FileServer(this.config, this.request, this);
    return fileServer.serveDownload(filePath, filename);
  }

  /**
   *
   */
  private processJson<DataType>(
    response?: APIResponse<DataType>,
    options?: {
      onSuccess?: (
        response: APIResponse<DataType> | null | undefined
      ) => boolean;
      onError?: (response: APIResponse<DataType> | null | undefined) => boolean;
    }
  ) {
    const {
      statusCode = 200,
      message,
      data = {},
      headers = {},
      ttl = 0,
    } = response || {};

    const { onSuccess, onError } = options || {};

    if (statusCode < 300) {
      return this.status(statusCode)
        .setHeaders(headers)
        .json({
          status: 'success',
          statusCode,
          message: message || 'Request successful',
          data,
          ttl,
        })
        .then((result) => onSuccess?.(response) || result);
    }

    return this.status(statusCode)
      .setHeaders(headers)
      .json({
        status: 'error',
        statusCode,
        message: message || 'Request failed',
        data,
        ttl,
      })
      .then((result) => onError?.(response) || result);
  }

  /**
   * sends json error data back to the client
   * @param statusCode http error status code, defaults to 400
   * @param message short message to return to client
   * @param errorData error data object, defaults to empty object
   */
  jsonError(response?: APIResponse): Promise<boolean> {
    return this.processJson({
      statusCode: 400,
      ...response,
    });
  }

  /**
   * sends json success data back to the client
   * @param statusCode http status code, defaults to 200
   * @param message short message to return to client
   * @param successData success data object, default to empty object
   */
  jsonSuccess(response?: APIResponse): Promise<boolean> {
    return this.processJson({
      statusCode: 200,
      ...response,
    });
  }

  /**
   * waits for the given time
   */
  wait(time: number): Promise<Response> {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(this);
      }, time);
    });
  }

  /**
   * it process a json api response, automatically handling
   * the then and catch responses,
   * @param response
   * @returns
   */
  async processJSONApi<DataType>(
    responsePromise: ReturnType<APIExecutor<any, DataType>>,
    options?: {
      onSuccess?: (response: APIResponse<DataType> | null | undefined) => any;
      onError?: (response: APIResponse<DataType> | null | undefined) => any;
    }
  ) {
    return responsePromise
      .then((response) =>
        this.processJson(
          {
            statusCode: 200,
            ...response,
          },
          options
        )
      )
      .catch((err) => {
        // handle errors
        if (err instanceof Error) {
          return handleError(err, this);
        }
        return this.processJson(
          {
            statusCode: 400,
            ...err,
          },
          options
        );
      });
  }
}
