import {
  Body,
  Controller,
  Get,
  Param,
  Patch,
  Post,
  Query,
  Req,
  UseGuards,
} from "@nestjs/common";
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 {
  BankCleared,
  BankDirection,
  BankService,
  BankSort,
} from "./bank.service";
import { CreateBankMovementDto } from "./bank-movement.dto";
import {
  CreateBankAccountDto,
  CreateBankDto,
  UpdateBankAccountDto,
  UpdateBankDto,
} from "./bank-account.dto";

const DIRECTIONS: BankDirection[] = ["income", "expense", "void"];
const CLEARED: BankCleared[] = ["cleared", "pending"];
const SORTS: BankSort[] = [
  "date_desc",
  "date_asc",
  "amount_desc",
  "amount_asc",
  "reference",
];

function one<T>(allowed: T[], value: string | undefined): T | undefined {
  return allowed.includes(value as T) ? (value as T) : 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("bank")
export class BankController {
  constructor(
    private readonly bank: BankService,
    private readonly audit: AuditService,
  ) {}

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

  // --- accounts -------------------------------------------------------------
  // Declared before the parameterised routes below so `/bank/accounts` can
  // never be swallowed by a `:id`-shaped path.

  /**
   * The account picker. Readable by any authenticated user, VIEWER included —
   * nothing else on this page can render until an account is chosen.
   */
  @Get("accounts")
  accounts() {
    return this.bank.listAccounts();
  }

  @Get("banks")
  banks() {
    return this.bank.listBanks();
  }

  @Post("banks")
  @RequireAbility("bank:manage-accounts")
  async createBank(@Body() dto: CreateBankDto, @Req() req: Request) {
    const row = await this.bank.createBank(dto);
    void this.audit.log(this.actingId(req), "bank.bank.create", {
      bankId: row.id,
      name: row.name,
    });
    return row;
  }

  @Patch("banks/:id")
  @RequireAbility("bank:manage-accounts")
  async updateBank(
    @Param("id") id: string,
    @Body() dto: UpdateBankDto,
    @Req() req: Request,
  ) {
    const row = await this.bank.updateBank(id, dto);
    void this.audit.log(this.actingId(req), "bank.bank.update", { bankId: id });
    return row;
  }

  @Post("accounts")
  @RequireAbility("bank:manage-accounts")
  async createAccount(
    @Body() dto: CreateBankAccountDto,
    @Req() req: Request,
  ) {
    const row = await this.bank.createAccount(dto);
    void this.audit.log(this.actingId(req), "bank.account.create", {
      bankAccountId: row.id,
      label: row.label,
      currency: row.currency,
    });
    return row;
  }

  @Patch("accounts/:id")
  @RequireAbility("bank:manage-accounts")
  async updateAccount(
    @Param("id") id: string,
    @Body() dto: UpdateBankAccountDto,
    @Req() req: Request,
  ) {
    const row = await this.bank.updateAccount(id, dto);
    void this.audit.log(this.actingId(req), "bank.account.update", {
      bankAccountId: id,
    });
    return row;
  }

  // --- register reads (all scoped to one account) ---------------------------

  @Get("stats")
  async stats(@Query("bankAccountId") bankAccountId?: string) {
    const account = await this.bank.requireAccount(bankAccountId);
    return this.bank.stats(account.id);
  }

  @Get("facets")
  async facets(@Query("bankAccountId") bankAccountId?: string) {
    const account = await this.bank.requireAccount(bankAccountId);
    return this.bank.facets(account.id);
  }

  /** Year and month rollups with a running net-movement figure. */
  @Get("summary")
  async summary(
    @Query("bankAccountId") bankAccountId?: string,
    @Query("year") year?: string,
  ) {
    const account = await this.bank.requireAccount(bankAccountId);
    const y = Number(year);
    return this.bank.summary(
      account.id,
      Number.isInteger(y) && y >= 1900 && y <= 2999 ? y : undefined,
    );
  }

  /** The register browser. */
  @Get()
  async list(
    @Query("bankAccountId") bankAccountId?: string,
    @Query("query") query?: string,
    @Query("page") page?: string,
    @Query("pageSize") pageSize?: string,
    @Query("direction") direction?: string,
    @Query("cleared") cleared?: string,
    @Query("from") from?: string,
    @Query("to") to?: string,
    @Query("sort") sort?: string,
  ) {
    const account = await this.bank.requireAccount(bankAccountId);
    return this.bank.list({
      bankAccountId: account.id,
      query,
      page: Math.max(1, Number(page) || 1),
      pageSize: Math.min(100, Math.max(1, Number(pageSize) || 25)),
      direction: one(DIRECTIONS, direction),
      cleared: one(CLEARED, cleared),
      from: parseDate(from),
      to: parseDate(to, true),
      sort: one(SORTS, sort) ?? "date_desc",
    });
  }

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

  @Post()
  @RequireAbility("bank:create")
  async create(@Body() dto: CreateBankMovementDto, @Req() req: Request) {
    const row = await this.bank.createMovement(dto);
    void this.audit.log(this.actingId(req), "bank.create", {
      bankTransactionId: row.id,
      bankAccountId: row.bankAccountId,
      amount: dto.amount,
    });
    return row;
  }

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