import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Param,
  Post,
  Query,
  Req,
  UseGuards,
} from "@nestjs/common";
import { TransactionDomain } from "@jorgecuadros/database";
import { Request } 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 {
  BalanceFilter,
  BalanceSort,
  BillingService,
  LedgerCurrency,
  LedgerDirection,
  MovementSort,
} from "./billing.service";
import {
  BatchCreateDto,
  CreateMovementDto,
  ResolveOutstandingDto,
} from "./movement.dto";

const DOMAINS: TransactionDomain[] = ["UTILITY", "INSURANCE", "TRUST"];
const CURRENCIES: LedgerCurrency[] = ["MXN", "USD"];
const DIRECTIONS: LedgerDirection[] = ["charge", "credit"];
const BALANCES: BalanceFilter[] = ["all", "owing", "credit", "settled"];
const MOVEMENT_SORTS: MovementSort[] = [
  "date_desc",
  "date_asc",
  "amount_desc",
  "amount_asc",
  "customer",
];
const BALANCE_SORTS: BalanceSort[] = [
  "owing_desc",
  "credit_desc",
  "recent",
  "customer",
];

function one<T>(allowed: T[], value: string | undefined): T | undefined {
  return allowed.includes(value as T) ? (value as T) : undefined;
}

/** Tri-state query flag: "true"/"false" filter, anything else means no filter. */
function flag(v: string | undefined): boolean | undefined {
  return v === "true" ? true : v === "false" ? false : undefined;
}

/** A `YYYY-MM-DD` bound; anything unparseable is treated as absent. */
function parseDate(v: string | undefined, endOfDay = false): Date | undefined {
  if (!v) return undefined;
  const d = new Date(endOfDay ? `${v}T23:59:59.999Z` : `${v}T00:00:00.000Z`);
  return Number.isNaN(d.getTime()) ? undefined : d;
}

@UseGuards(AuthenticatedGuard, AbilityGuard)
@Controller("billing")
export class BillingController {
  constructor(
    private readonly billing: BillingService,
    private readonly audit: AuditService,
  ) {}

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

  @Get("stats")
  stats() {
    return this.billing.stats();
  }

  @Get("facets")
  facets() {
    return this.billing.facets();
  }

  /** Per-customer balances — the receivables worklist. */
  @Get("balances")
  balances(
    @Query("query") query?: string,
    @Query("page") page?: string,
    @Query("pageSize") pageSize?: string,
    @Query("currency") currency?: string,
    @Query("balance") balance?: string,
    @Query("domain") domain?: string,
    @Query("sort") sort?: string,
  ) {
    return this.billing.balances({
      query,
      page: Math.max(1, Number(page) || 1),
      pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)),
      currency: one(CURRENCIES, currency) ?? "MXN",
      balance: one(BALANCES, balance) ?? "all",
      domain: one(DOMAINS, domain),
      sort: one(BALANCE_SORTS, sort) ?? "owing_desc",
    });
  }

  /**
   * Every movement cut against one check, with its total — the reconciliation
   * view replacing the legacy REPORTE CHEQUE COUNT. Declared before the
   * `customers/:id` and `:id`-shaped routes so the literal path wins.
   */
  @Get("by-check")
  byCheck(@Query("checkNumber") checkNumber?: string) {
    const n = checkNumber?.trim();
    if (!n) throw new BadRequestException("checkNumber es obligatorio");
    return this.billing.byCheck(n);
  }

  /** One customer's full statement across both business lines. */
  @Get("customers/:id")
  statement(@Param("id") id: string) {
    return this.billing.statement(id);
  }

  /** Cross-customer movement browser. */
  @Get()
  movements(
    @Query("query") query?: string,
    @Query("page") page?: string,
    @Query("pageSize") pageSize?: string,
    @Query("domain") domain?: string,
    @Query("currency") currency?: string,
    @Query("direction") direction?: string,
    @Query("typeId") typeId?: string,
    @Query("source") source?: string,
    @Query("customerId") customerId?: string,
    @Query("outstanding") outstanding?: string,
    @Query("checkNumber") checkNumber?: string,
    @Query("from") from?: string,
    @Query("to") to?: string,
    @Query("sort") sort?: string,
  ) {
    return this.billing.movements({
      query,
      page: Math.max(1, Number(page) || 1),
      pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)),
      domain: one(DOMAINS, domain),
      currency: one(CURRENCIES, currency),
      direction: one(DIRECTIONS, direction),
      typeId: typeId || undefined,
      source: source || undefined,
      customerId: customerId || undefined,
      outstanding: flag(outstanding),
      checkNumber: checkNumber?.trim() || undefined,
      from: parseDate(from),
      to: parseDate(to, true),
      sort: one(MOVEMENT_SORTS, sort) ?? "date_desc",
    });
  }

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

  @Post()
  @RequireAbility("ledger:create")
  async create(@Body() dto: CreateMovementDto, @Req() req: Request) {
    const tx = await this.billing.createMovement(dto);
    void this.audit.log(this.actingId(req), "ledger.create", {
      transactionId: tx.id,
      customerId: dto.customerId,
      amount: dto.amount,
      currency: tx.currency,
    });
    return tx;
  }

  /**
   * Batch capture: many customers' receipts against one physical check.
   * Same ability as single capture — batching is still capturing.
   */
  @Post("batch")
  @RequireAbility("ledger:create")
  async createBatch(@Body() dto: BatchCreateDto, @Req() req: Request) {
    const result = await this.billing.createBatch(dto);
    void this.audit.log(this.actingId(req), "ledger.batch", {
      checkNumber: dto.checkNumber,
      count: result.count,
      total: result.total,
      currency: result.currency,
    });
    return result;
  }

  /**
   * Resolve an outstanding (NOPAGO) row — `ledger:create`, not `ledger:void`:
   * resolving completes a capture, it doesn't reverse one.
   */
  @Post(":id/resolve-outstanding")
  @RequireAbility("ledger:create")
  async resolveOutstanding(
    @Param("id") id: string,
    @Body() dto: ResolveOutstandingDto,
    @Req() req: Request,
  ) {
    const tx = await this.billing.resolveOutstanding(id, dto);
    void this.audit.log(this.actingId(req), "ledger.resolve-outstanding", {
      transactionId: id,
      checkNumber: dto.checkNumber,
    });
    return tx;
  }

  @Post(":id/void")
  @RequireAbility("ledger:void")
  async void(@Param("id") id: string, @Req() req: Request) {
    const tx = await this.billing.voidMovement(id, this.actingId(req));
    void this.audit.log(this.actingId(req), "ledger.void", { transactionId: id });
    return tx;
  }
}
