import {
  Body,
  Controller,
  Get,
  Param,
  Patch,
  Post,
  Query,
  Req,
  Res,
  StreamableFile,
  UploadedFiles,
  UseGuards,
  UseInterceptors,
} from "@nestjs/common";
import { FilesInterceptor } from "@nestjs/platform-express";
import type { ServiceKind, StatementDocumentStatus } from "@jorgecuadros/database";
import type { Request, Response } from "express";
import { AuthenticatedGuard } from "../auth/authenticated.guard";
import { AbilityGuard } from "../auth/ability.guard";
import { RequireAbility } from "../auth/require-ability.decorator";
import { AuditService } from "../common/audit.service";
import type { UploadedFileLike } from "../storage/upload-file";
import { StatementsService } from "./statements.service";
import { ConfirmBatchDto, ReviewDocumentDto } from "./statement.dto";

/**
 * Statement OCR intake (RECEIPT_CAPTURE_SPEC §2).
 *
 * Nothing here writes to the ledger directly — confirming a batch delegates to
 * BillingService, so an OCR-captured charge is indistinguishable from a
 * hand-keyed one except for its `captureSource`.
 */
@Controller("statements")
@UseGuards(AuthenticatedGuard, AbilityGuard)
export class StatementsController {
  constructor(
    private readonly statements: StatementsService,
    private readonly audit: AuditService,
  ) {}

  private actingId(req: Request): string {
    return (req.user as { id: string } | undefined)?.id ?? "";
  }

  /**
   * Whether this deployment can ingest scans at all — the UI hides automatic
   * capture without it. Both halves are needed: OCR to read the page, object
   * storage to keep it.
   */
  @Get("status")
  async status() {
    return {
      ocrAvailable: await this.statements.ocrAvailable(),
      storageAvailable: this.statements.storageAvailable(),
    };
  }

  @Get("batches")
  listBatches(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
    return this.statements.listBatches(
      Math.max(1, Number(page) || 1),
      Math.min(100, Math.max(1, Number(pageSize) || 25)),
    );
  }

  @Get("batches/:id")
  getBatch(@Param("id") id: string) {
    return this.statements.getBatch(id);
  }

  @Get("batches/:id/documents")
  listDocuments(@Param("id") id: string, @Query("status") status?: string) {
    return this.statements.listDocuments(
      id,
      (status || undefined) as StatementDocumentStatus | undefined,
    );
  }

  /** The rendered page, so a reviewer can compare it against what was read. */
  @Get("documents/:id/page")
  async pageImage(@Param("id") id: string, @Res({ passthrough: true }) res: Response) {
    const { stream, contentType, contentLength } = await this.statements.pageImage(id);
    res.set({
      "Content-Type": contentType ?? "image/png",
      ...(contentLength ? { "Content-Length": String(contentLength) } : {}),
    });
    return new StreamableFile(stream);
  }

  // --- writes ---------------------------------------------------------------

  @Post("batches")
  @RequireAbility("statement:ingest")
  @UseInterceptors(
    // A month of one company's statements is a handful of multi-page scans;
    // 25 files at 50MB covers that with room to spare.
    FilesInterceptor("files", 25, { limits: { fileSize: 50 * 1024 * 1024 } }),
  )
  async createBatch(
    @UploadedFiles() files: UploadedFileLike[] | undefined,
    @Query("serviceKind") serviceKind: ServiceKind,
    @Query("label") label: string | undefined,
    @Req() req: Request,
  ) {
    const batch = await this.statements.createBatch(
      files ?? [],
      serviceKind,
      this.actingId(req),
      label,
    );
    void this.audit.log(this.actingId(req), "statement.batch.create", {
      batchId: batch.id,
      serviceKind,
      fileCount: batch.fileCount,
    });
    return batch;
  }

  @Patch("documents/:id")
  @RequireAbility("statement:review")
  async review(
    @Param("id") id: string,
    @Body() dto: ReviewDocumentDto,
    @Req() req: Request,
  ) {
    const doc = await this.statements.review(id, dto, this.actingId(req));
    void this.audit.log(this.actingId(req), "statement.document.review", {
      documentId: id,
      status: doc.status,
    });
    return doc;
  }

  @Post("documents/:id/reject")
  @RequireAbility("statement:review")
  async reject(@Param("id") id: string, @Req() req: Request) {
    const doc = await this.statements.reject(id, this.actingId(req));
    void this.audit.log(this.actingId(req), "statement.document.reject", {
      documentId: id,
    });
    return doc;
  }

  /** Abandon a batch pending review — rejects every unposted page. */
  @Post("batches/:id/discard")
  @RequireAbility("statement:review")
  async discard(@Param("id") id: string, @Req() req: Request) {
    const result = await this.statements.discardBatch(id, this.actingId(req));
    void this.audit.log(this.actingId(req), "statement.batch.discard", {
      batchId: id,
      rejected: result.rejected,
    });
    return result;
  }

  /** Post every matched document in the batch, against one check. */
  @Post("batches/:id/confirm")
  @RequireAbility("statement:review")
  async confirm(
    @Param("id") id: string,
    @Body() dto: ConfirmBatchDto,
    @Req() req: Request,
  ) {
    const result = await this.statements.confirmBatch(id, dto, this.actingId(req));
    void this.audit.log(this.actingId(req), "statement.batch.confirm", {
      batchId: id,
      posted: result.posted,
      total: result.total,
      checkNumber: dto.checkNumber,
    });
    return result;
  }
}
