import type {Server} from "node:http"
import type {AddressInfo, ListenOptions} from "node:net"
import {
  type Res,
  SkipResponse,
  type StatusCode,
} from "@nahkies/typescript-common-runtime/types"
import type {OptionsJson, OptionsText, OptionsUrlencoded} from "body-parser"
import Cors, {type CorsOptions, type CorsOptionsDelegate} from "cors"
import type {Response} from "express"
import express, {
  type ErrorRequestHandler,
  type Express,
  type Response as ExpressResponse,
  type Request,
  type RequestHandler,
  type Router,
} from "express"
import {ExpressRuntimeError} from "./errors.ts"

export {parseQueryParameters} from "@nahkies/typescript-common-runtime/query-parser"

import {
  parseOctetStreamRequestBody,
  type SizeLimit,
} from "@nahkies/typescript-common-runtime/request-bodies/octet-stream"

export {
  type Params,
  type Res,
  SkipResponse,
  type StatusCode,
  type StatusCode1xx,
  type StatusCode2xx,
  type StatusCode3xx,
  type StatusCode4xx,
  type StatusCode5xx,
} from "@nahkies/typescript-common-runtime/types"

// biome-ignore lint/suspicious/noExplicitAny: needed
export type ResponseValidator = (status: number, value: unknown) => any

export class ExpressRuntimeResponse<Type> {
  private _body?: Type

  constructor(private readonly status: StatusCode) {}

  body(body: Type): this {
    this._body = body
    return this
  }

  unpack(): Res<StatusCode, Type | undefined> {
    return {status: this.status, body: this._body}
  }
}

export function handleResponse(
  res: Response,
  validator: (status: number, value: unknown) => unknown,
) {
  return async (
    response:
      | ExpressRuntimeResponse<unknown>
      | typeof SkipResponse
      | Res<StatusCode, unknown>,
  ): Promise<void> => {
    // escape hatch to allow responses to be sent by the implementation handler
    if (response === SkipResponse) {
      return
    }

    const {status, body} =
      response instanceof ExpressRuntimeResponse ? response.unpack() : response

    res.status(status)

    if (body === undefined) {
      res.end()
      return
    }

    if (body instanceof Blob) {
      await sendBlob(res, body)
    } else {
      res.json(validator(status, body))
    }
  }
}

export function handleImplementationError(err: unknown): never {
  throw ExpressRuntimeError.HandlerError(err)
}

export type ExpressRuntimeResponder<
  Status extends StatusCode = StatusCode,
  // biome-ignore lint/suspicious/noExplicitAny: needed
  Type = any,
> = {
  withStatus: (status: Status) => ExpressRuntimeResponse<Type>
}

export type ServerConfig = {
  /**
   * set to "disabled" to disable cors middleware, omit or pass undefined for defaults
   *
   * by default, all origins are allowed. you probably don't want this in production,
   * so it's strongly recommended to explicitly configure this.
   **/
  cors?: "disabled" | CorsOptions | CorsOptionsDelegate | undefined

  /**
   * set to "disabled" to disable body parsing middleware, omit or pass undefined for defaults.
   *
   * if disabling, ensure you pass a body parsing middleware that places the parsed
   * body on `req.body` for request body processing to work.
   **/
  body?:
    | "disabled"
    | Partial<{
        json: OptionsJson
        text: OptionsText
        urlencoded: OptionsUrlencoded
      }>
    | undefined

  /**
   * provide arbitrary express middleware to be mounted before all request handlers
   * useful for mounting logging, alternative body parsers, etc
   */
  middleware?: RequestHandler[]

  /**
   * Provide a custom 404 handler
   */
  notFoundHandler?: RequestHandler

  /**
   * Provide a custom error handler
   */
  errorHandler?: ErrorRequestHandler

  /**
   * the router to use, normally obtained by calling the generated `createRouter`
   * function
   */
  router: Router

  /**
   * the port to listen on, a randomly allocated port will be used if none passed
   * alternatively ListenOptions can be passed to control the network interface
   * bound to.
   */
  port?: number | ListenOptions
}

/**
 * Starts an Express server and listens on `port` or a randomly allocated port if none provided.
 * Enables CORS and body parsing by default. It's recommended to customize the CORS options
 * for production usage.
 *
 * If you need more control over your Express server you should avoid calling this function,
 * and instead mount the router from your generated codes `createRouter` call directly
 * onto a server you have constructed.
 */
export async function startServer({
  middleware = [],
  cors = undefined,
  body = undefined,
  port = 0,
  router,
  notFoundHandler,
  errorHandler,
}: ServerConfig): Promise<{
  app: Express
  server: Server
  address: AddressInfo
}> {
  const app = express()

  if (cors !== "disabled") {
    app.use(Cors(cors))
    app.options("*route", Cors(cors))
  }

  if (body !== "disabled") {
    app.use(express.json(body?.json))
    app.use(express.text(body?.text))
    app.use(express.urlencoded(body?.urlencoded ?? {extended: true}))
  }

  if (middleware) {
    for (const it of middleware) {
      app.use(it)
    }
  }

  app.use(router)

  if (notFoundHandler) {
    app.use(notFoundHandler)
  }

  if (errorHandler) {
    app.use(errorHandler)
  }

  return new Promise((resolve, reject) => {
    try {
      const server = app.listen(port)

      server.once("listening", () => {
        try {
          const address = server.address()

          if (!address || typeof address !== "object") {
            throw new Error("failed to bind port")
          }

          resolve({app, server, address})
        } catch (err) {
          reject(err)
        }
      })

      server.once("error", (err) => {
        reject(err)
      })
    } catch (err) {
      reject(err)
    }
  })
}

export async function parseOctetStream(
  req: Request,
  sizeLimit: SizeLimit,
): Promise<Blob | undefined> {
  return parseOctetStreamRequestBody(req, {sizeLimit})
}

export async function sendBlob(res: ExpressResponse, body: Blob) {
  const arrayBuffer = await body.arrayBuffer()
  const buffer = Buffer.from(arrayBuffer)

  res.setHeader("Content-Type", body.type ?? "application/octet-stream")
  res.setHeader("Content-Length", buffer.length)

  res.send(buffer)
}
