import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { NextFunction, Request, Response } from "express";
import httpStatus from "http-status";
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper";
import {
  ApiError,
  ConflictApiError,
  ForbiddenApiError,
  InternalApiError,
  NotFoundApiError,
  UnauthorizedApiError,
  UnavailableApiError,
  ValidationApiError,
} from "./errors";
import { GuardFn } from "./express-guards";

export abstract class ExpressController {
  protected req!: Request;
  protected res!: Response;
  protected next!: NextFunction;
  protected criteria!: Criteria;
  protected url!: URL;
  protected errorMapper!: ApiErrorMapper;

  // 🔹 Guards configurables por controlador
  private guards: GuardFn[] = [];

  static errorResponse(apiError: ApiError, req: Request, res: Response) {
    const ctx = {
      instance: req.originalUrl,
      correlationId: req.get("x-correlation-id") || undefined,
      method: req.method,
    } satisfies ApiErrorContext;

    const body = toProblemJson(apiError, ctx);
    return res.type("application/problem+json").status(apiError.status).json(body);
  }

  public constructor() {
    this.errorMapper = ApiErrorMapper.default();
  }

  // ───────────────────────────────────────────────────────────────────────────
  // Método principal
  public async execute(req: Request, res: Response, next: NextFunction): Promise<void> {
    this.req = req;
    this.res = res;
    this.next = next;
    this.url = this.buildUrlFromRequest();

    try {
      this.criteria = this.buildCriteriaFromURL();

      // Ejecutar guards (auth/tenant/otros). Si alguno falla, se responde y no se entra al impl.
      const ok = await this.runGuards();
      if (!ok) return;

      await this.executeImpl();
    } catch (error: unknown) {
      this.handleError(error as Error);
    }
  }

  protected abstract executeImpl(): Promise<unknown>;

  // ───────────────────────────────────────────────────────────────────────────
  // Helpers de auth/tenant (opcionales para usar en executeImpl)

  public getUser<T extends { userId?: UniqueID; companyId?: UniqueID } = any>(): T | undefined {
    // Si usáis un tipo RequestWithAuth real, cámbialo aquí
    return (this.req as any).user as T | undefined;
  }

  public getTenantId(): UniqueID | undefined {
    const user = this.getUser<{ companyId?: UniqueID }>();
    return user?.companyId;
  }

  // ───────────────────────────────────────────────────────────────────────────

  protected buildUrlFromRequest(): URL {
    // Si Express está tras un proxy y trust proxy=true, estos headers son fiables
    const host = this.req.get("x-forwarded-host") ?? this.req.get("host") ?? "localhost";
    const proto = this.req.get("x-forwarded-proto") ?? this.req.protocol ?? "http";
    return new URL(this.req.originalUrl || "/", `${proto}://${host}`);
  }

  protected buildCriteriaFromURL() {
    return new CriteriaFromUrlConverter().toCriteria(this.url);
  }

  protected ok<T>(dto?: T, headers?: Record<string, string>) {
    if (headers) Object.entries(headers).forEach(([k, v]) => this.res.setHeader(k, v));
    return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.sendStatus(httpStatus.OK);
  }

  protected created<T>(dto?: T, location?: string) {
    if (location) this.res.setHeader("Location", location);
    return dto
      ? this.res.status(httpStatus.CREATED).json(dto)
      : this.res.sendStatus(httpStatus.CREATED);
  }

  protected noContent() {
    return this.res.sendStatus(httpStatus.NO_CONTENT);
  }

  public sendFile(filepath: string) {
    return this.res.sendFile(filepath);
  }

  public downloadFile(filepath: string, filename: string, done?: any) {
    return this.res.download(filepath, filename, done);
  }

  public downloadPDF(pdfBuffer: Buffer, filename: string) {
    this.res.set({
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename=${filename}`,
      "Content-Length": pdfBuffer.length,
    });

    return this.res.send(pdfBuffer);
  }

  protected clientError(message: string, errors?: any[] | any) {
    return this.handleApiError(
      new ValidationApiError(message, Array.isArray(errors) ? errors : [errors])
    );
  }

  protected unauthorizedError(message?: string) {
    return this.handleApiError(new UnauthorizedApiError(message ?? "Unauthorized"));
  }

  protected forbiddenError(message?: string) {
    return this.handleApiError(
      new ForbiddenApiError(message ?? "You do not have permission to perform this action.")
    );
  }

  protected notFoundError(message: string) {
    return this.handleApiError(new NotFoundApiError(message));
  }

  protected conflictError(message: string) {
    return this.handleApiError(new ConflictApiError(message));
  }

  protected invalidInputError(message: string, errors?: any[]) {
    return this.handleApiError(new ValidationApiError(message, errors));
  }

  protected unavailableError(message?: string) {
    return this.handleApiError(
      new UnavailableApiError(message ?? "Service temporarily unavailable.")
    );
  }

  protected internalServerError(message?: string) {
    return this.handleApiError(new InternalApiError(message ?? "Internal Server Error"));
  }

  protected handleApiError(apiError: ApiError) {
    return ExpressController.errorResponse(apiError, this.req, this.res);
  }

  protected handleError(error: unknown, ctx?: ApiErrorContext) {
    const err = error instanceof Error ? error : new Error("Unknown error");
    const apiError = this.errorMapper.map(err, ctx);
    return this.handleApiError(apiError);
  }

  // ───────────────────────────────────────────────────────────────────────────
  // Guards API
  protected useGuards(...guards: GuardFn[]): this {
    this.guards.push(...guards);
    return this;
  }

  private async runGuards(): Promise<boolean> {
    for (const guard of this.guards) {
      const result = await guard({
        req: this.req,
        res: this.res,
        next: this.next,
        controller: this,
        criteria: this.criteria,
      });
      if (result?.isFailure) {
        const err = result.error ?? new InternalApiError("Guard failed without error");
        this.handleApiError(err);
        return false;
      }
    }
    return true;
  }
}
