import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  Entity, PrimaryGeneratedColumn, Column,
  CreateDateColumn, UpdateDateColumn, OneToMany, ManyToOne, JoinColumn,
} from 'typeorm';
import {
  Injectable, NotFoundException, BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import {
  Controller, Get, Post, Put, Patch,
  Body, Param, Query, UseGuards, Request,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import {
  IsString, IsOptional, IsNumber, IsBoolean,
  IsUUID, IsDateString, IsIn, Min, MinLength,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

// ============================================================
// VALOR POR EXTENSO (pt-BR) — usado no mnemônico {{VALOR_TOTAL_EXTENSO}}
// ============================================================
const UNIDADES = ['', 'um', 'dois', 'três', 'quatro', 'cinco', 'seis', 'sete', 'oito', 'nove'];
const DEZ_A_DEZENOVE = ['dez', 'onze', 'doze', 'treze', 'quatorze', 'quinze', 'dezesseis', 'dezessete', 'dezoito', 'dezenove'];
const DEZENAS = ['', '', 'vinte', 'trinta', 'quarenta', 'cinquenta', 'sessenta', 'setenta', 'oitenta', 'noventa'];
const CENTENAS = ['', 'cento', 'duzentos', 'trezentos', 'quatrocentos', 'quinhentos', 'seiscentos', 'setecentos', 'oitocentos', 'novecentos'];

function grupoPorExtenso(n: number): string {
  if (n === 0) return '';
  if (n === 100) return 'cem';
  const c = Math.floor(n / 100);
  const resto = n % 100;
  const partes: string[] = [];
  if (c > 0) partes.push(CENTENAS[c]);
  if (resto > 0) {
    if (resto < 10) partes.push(UNIDADES[resto]);
    else if (resto < 20) partes.push(DEZ_A_DEZENOVE[resto - 10]);
    else {
      const d = Math.floor(resto / 10);
      const u = resto % 10;
      partes.push(u > 0 ? `${DEZENAS[d]} e ${UNIDADES[u]}` : DEZENAS[d]);
    }
  }
  return partes.join(' e ');
}

function inteiroPorExtenso(n: number): string {
  if (n === 0) return 'zero';
  const milhoes = Math.floor(n / 1000000);
  const milhares = Math.floor((n % 1000000) / 1000);
  const centenas = n % 1000;

  const grupos: { texto: string; usaEComOAnterior: boolean }[] = [];
  if (milhoes > 0) {
    const ehUltimoGrupo = milhares === 0 && centenas === 0;
    const base = milhoes === 1 ? 'um milhão' : `${grupoPorExtenso(milhoes)} milhões`;
    grupos.push({ texto: ehUltimoGrupo ? `${base} de` : base, usaEComOAnterior: false });
  }
  if (milhares > 0) {
    grupos.push({ texto: milhares === 1 ? 'um mil' : `${grupoPorExtenso(milhares)} mil`, usaEComOAnterior: milhares < 100 || milhares % 100 === 0 });
  }
  if (centenas > 0) {
    grupos.push({ texto: grupoPorExtenso(centenas), usaEComOAnterior: centenas < 100 || centenas % 100 === 0 });
  }

  let resultado = grupos[0].texto;
  for (let i = 1; i < grupos.length; i++) {
    resultado += (grupos[i].usaEComOAnterior ? ' e ' : ', ') + grupos[i].texto;
  }
  return resultado;
}

function valorPorExtenso(valor: number): string {
  const reais = Math.floor(Math.abs(valor));
  const centavos = Math.round((Math.abs(valor) - reais) * 100);
  const partes: string[] = [];
  if (reais > 0 || centavos === 0) partes.push(`${inteiroPorExtenso(reais)} ${reais === 1 ? 'real' : 'reais'}`);
  if (centavos > 0) partes.push(`${inteiroPorExtenso(centavos)} ${centavos === 1 ? 'centavo' : 'centavos'}`);
  return partes.join(' e ');
}

// ============================================================
// ENTITIES
// ============================================================
@Entity('contracts')
export class Contract {
  @PrimaryGeneratedColumn('uuid') id: string;
  @Column({ name: 'tenant_id' }) tenantId: string;
  @Column({ name: 'proposal_id', nullable: true }) proposalId: string;
  @Column({ name: 'client_id' }) clientId: string;
  @Column({ name: 'created_by', nullable: true }) createdBy: string;
  @Column({ length: 20 }) number: string;
  @Column({ length: 200, nullable: true }) title: string;
  @Column({ name: 'start_date', type: 'date' }) startDate: string;
  @Column({ name: 'end_date', type: 'date', nullable: true }) endDate: string;
  @Column({ name: 'adjustment_index', default: 'IPCA' }) adjustmentIndex: string;
  @Column({ name: 'adjustment_rate', type: 'numeric', precision: 5, scale: 2, nullable: true }) adjustmentRate: number;
  @Column({ name: 'adjustment_month', type: 'smallint', nullable: true }) adjustmentMonth: number;
  @Column({ name: 'notice_days', default: 30 }) noticeDays: number;
  @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) subtotal: number;
  @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) discount: number;
  @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) total: number;
  @Column({ name: 'autentique_doc_id', nullable: true }) autentiqueDocId: string;
  @Column({ name: 'autentique_doc_url', nullable: true }) autentiqueDocUrl: string;
  @Column({ name: 'autentique_status', nullable: true }) autentiqueStatus: string;
  @Column({ name: 'signed_at', type: 'timestamptz', nullable: true }) signedAt: Date;
  @Column({ name: 'billing_day', type: 'smallint', default: 5 }) billingDay: number;
  @Column({ name: 'payment_terms', default: 30 }) paymentTerms: number;
  @Column({ name: 'invoice_trigger', default: 'after_payment' }) invoiceTrigger: string;
  @Column({ name: 'invoice_days_before', default: 0 }) invoiceDaysBefore: number;
  // Parcelamento — opcional; contratos à vista/recorrentes deixam nulo.
  @Column({ name: 'installments_count', type: 'smallint', nullable: true }) installmentsCount: number;
  @Column({ name: 'installment_amount', type: 'numeric', precision: 15, scale: 2, nullable: true }) installmentAmount: number;
  @Column({ name: 'first_due_date', type: 'date', nullable: true }) firstDueDate: string;
  @Column({ type: 'text', nullable: true }) notes: string;
  @Column({ name: 'internal_notes', type: 'text', nullable: true }) internalNotes: string;
  @Column({ default: 'draft' }) status: string;
  @Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true }) cancelledAt: Date;
  @Column({ name: 'cancellation_reason', type: 'text', nullable: true }) cancellationReason: string;
  @OneToMany(() => ContractItem, (i) => i.contract, { cascade: true, eager: true })
  items: ContractItem[];
  @CreateDateColumn({ name: 'created_at' }) createdAt: Date;
  @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date;
}

@Entity('contract_items')
export class ContractItem {
  @PrimaryGeneratedColumn('uuid') id: string;
  @Column({ name: 'contract_id' }) contractId: string;
  @Column({ name: 'tenant_id' }) tenantId: string;
  @Column({ name: 'product_id', nullable: true }) productId: string;
  @Column({ name: 'product_variant_id', nullable: true }) productVariantId: string;
  @Column({ length: 150 }) name: string;
  @Column({ type: 'text', nullable: true }) description: string;
  @Column({ name: 'billing_cycle', nullable: true }) billingCycle: string;
  @Column({ name: 'unit_price', type: 'numeric', precision: 15, scale: 2 }) unitPrice: number;
  @Column({ type: 'numeric', precision: 10, scale: 3, default: 1 }) quantity: number;
  @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) discount: number;
  @Column({ type: 'numeric', precision: 15, scale: 2 }) total: number;
  @Column({ name: 'service_list_item', nullable: true }) serviceListItem: string;
  @Column({ name: 'iss_aliquota', type: 'numeric', precision: 4, scale: 2, nullable: true }) issAliquota: number;
  @Column({ type: 'text', nullable: true }) discrimination: string;
  @Column({ name: 'sort_order', default: 0 }) sortOrder: number;
  @ManyToOne(() => Contract, (c) => c.items, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'contract_id' })
  contract: Contract;
  @CreateDateColumn({ name: 'created_at' }) createdAt: Date;
}

// ============================================================
// DTOs
// ============================================================
export class CreateContractDto {
  @ApiPropertyOptional({ description: 'ID da proposta de origem' })
  @IsOptional() @IsUUID() proposalId?: string;

  @ApiPropertyOptional({ description: 'Ignorado ao criar a partir de proposta — vem da própria proposta' })
  @IsOptional() @IsUUID() clientId?: string;
  @ApiPropertyOptional() @IsOptional() @IsString() title?: string;
  @ApiProperty({ example: '2025-01-01' }) @IsDateString() startDate: string;
  @ApiPropertyOptional({ example: '2025-12-31' }) @IsOptional() @IsDateString() endDate?: string;

  @ApiPropertyOptional({ enum: ['IPCA', 'IGP-M', 'INPC', 'fixed', 'none'], default: 'IPCA' })
  @IsOptional() @IsIn(['IPCA', 'IGP-M', 'INPC', 'fixed', 'none']) adjustmentIndex?: string;

  @ApiPropertyOptional({ description: 'Taxa fixa de reajuste (%)' })
  @IsOptional() @IsNumber() adjustmentRate?: number;

  @ApiPropertyOptional({ description: 'Mês aniversário do reajuste (1-12)' })
  @IsOptional() @IsNumber() @Min(1) adjustmentMonth?: number;

  @ApiPropertyOptional({ default: 30 }) @IsOptional() @IsNumber() noticeDays?: number;
  @ApiPropertyOptional({ default: 5, description: 'Dia do mês para cobrança' })
  @IsOptional() @IsNumber() billingDay?: number;
  @ApiPropertyOptional({ default: 30, description: 'Prazo de pagamento em dias' })
  @IsOptional() @IsNumber() paymentTerms?: number;

  @ApiPropertyOptional({
    enum: ['on_sign', 'before_due', 'after_payment'],
    default: 'after_payment',
    description: 'Quando emitir a NFS-e',
  })
  @IsOptional() @IsIn(['on_sign', 'before_due', 'after_payment']) invoiceTrigger?: string;

  @ApiPropertyOptional() @IsOptional() @IsNumber() invoiceDaysBefore?: number;

  @ApiPropertyOptional({ description: 'Quantidade de parcelas (contratos parcelados)' })
  @IsOptional() @IsNumber() installmentsCount?: number;
  @ApiPropertyOptional({ description: 'Valor de cada parcela' })
  @IsOptional() @IsNumber() @Min(0) installmentAmount?: number;
  @ApiPropertyOptional({ example: '2025-04-30', description: 'Vencimento da 1ª parcela' })
  @IsOptional() @IsDateString() firstDueDate?: string;

  @ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
  @ApiPropertyOptional() @IsOptional() @IsString() internalNotes?: string;
}

export class CancelContractDto {
  @ApiProperty({ description: 'Motivo do cancelamento' })
  @IsString() @MinLength(5) reason: string;
}

export class TerminateContractDto {
  @ApiProperty({ description: 'Motivo do distrato' })
  @IsString() @MinLength(5) reason: string;

  @ApiPropertyOptional({ description: 'Data do distrato (padrão: hoje)', example: '2025-06-10' })
  @IsOptional() @IsDateString() terminationDate?: string;
}

// ============================================================
// SERVICE
// ============================================================
@Injectable()
export class ContractsService {
  constructor(
    @InjectRepository(Contract)
    private readonly contractRepo: Repository<Contract>,

    @InjectRepository(ContractItem)
    private readonly itemRepo: Repository<ContractItem>,

    private readonly dataSource: DataSource,
  ) {}

  private async nextNumber(tenantId: string): Promise<string> {
    const result = await this.dataSource.query(
      `INSERT INTO tenant_sequences (tenant_id, entity, last_value)
       VALUES ($1, 'contract', 1)
       ON CONFLICT (tenant_id, entity) DO UPDATE
       SET last_value = tenant_sequences.last_value + 1
       RETURNING last_value`,
      [tenantId],
    );
    const row = Array.isArray(result[0]) ? result[0][0] : result[0];
    const seq = String(row.last_value).padStart(5, '0');
    return `CONT-${new Date().getFullYear()}-${seq}`;
  }

  // Cria contrato a partir de uma proposta aprovada
  async createFromProposal(
    tenantId: string,
    userId: string,
    proposalId: string,
    dto: CreateContractDto,
  ): Promise<Contract> {
    const proposalResult = await this.dataSource.query(
      `SELECT p.*, json_agg(pi.*) as items
       FROM proposals p
       LEFT JOIN proposal_items pi ON pi.proposal_id = p.id
       WHERE p.id = $1 AND p.tenant_id = $2 AND p.status = 'accepted'
       GROUP BY p.id`,
      [proposalId, tenantId],
    );

    if (!proposalResult[0])
      throw new BadRequestException('Proposta não encontrada ou não está aceita');

    const proposal = proposalResult[0];
    return this.createWithItems(tenantId, userId, {
      ...dto,
      proposalId,
      clientId: proposal.client_id,
      items: proposal.items || [],
      subtotal: proposal.subtotal,
      discount: proposal.discount,
      total: proposal.total,
    });
  }

  private async createWithItems(tenantId: string, userId: string, data: any): Promise<Contract> {
    const number = await this.nextNumber(tenantId);
    const contract = this.contractRepo.create({
      tenantId,
      createdBy: userId,
      number,
      proposalId: data.proposalId,
      clientId: data.clientId,
      title: data.title,
      startDate: data.startDate,
      endDate: data.endDate,
      adjustmentIndex: data.adjustmentIndex ?? 'IPCA',
      adjustmentRate: data.adjustmentRate,
      adjustmentMonth: data.adjustmentMonth,
      noticeDays: data.noticeDays ?? 30,
      billingDay: data.billingDay ?? 5,
      paymentTerms: data.paymentTerms ?? 30,
      invoiceTrigger: data.invoiceTrigger ?? 'after_payment',
      invoiceDaysBefore: data.invoiceDaysBefore ?? 0,
      installmentsCount: data.installmentsCount ?? data.installments_count,
      installmentAmount: data.installmentAmount ?? data.installment_amount,
      firstDueDate: data.firstDueDate ?? data.first_due_date,
      notes: data.notes,
      internalNotes: data.internalNotes,
      subtotal: data.subtotal,
      discount: data.discount,
      total: data.total,
      status: 'draft',
      items: (data.items || []).map((i: any) =>
        this.itemRepo.create({
          tenantId,
          productId: i.productId ?? i.product_id,
          productVariantId: i.productVariantId ?? i.product_variant_id,
          name: i.name,
          description: i.description,
          billingCycle: i.billingCycle ?? i.billing_cycle,
          unitPrice: i.unitPrice ?? i.unit_price,
          quantity: i.quantity ?? 1,
          discount: i.discount ?? 0,
          total: i.total,
          serviceListItem: i.serviceListItem ?? i.service_list_item,
          issAliquota: i.issAliquota ?? i.iss_aliquota,
          discrimination: i.discrimination,
          sortOrder: i.sortOrder ?? i.sort_order ?? 0,
        }),
      ),
    });
    return this.contractRepo.save(contract);
  }

  async findAll(tenantId: string, status?: string, clientId?: string): Promise<Contract[]> {
    const qb = this.contractRepo.createQueryBuilder('c')
      .leftJoinAndSelect('c.items', 'items')
      .where('c.tenant_id = :tenantId', { tenantId })
      .orderBy('c.created_at', 'DESC');
    if (status) qb.andWhere('c.status = :status', { status });
    if (clientId) qb.andWhere('c.client_id = :clientId', { clientId });
    return qb.getMany();
  }

  async findOne(tenantId: string, id: string): Promise<Contract> {
    const c = await this.contractRepo.findOne({ where: { id, tenantId }, relations: ['items'] });
    if (!c) throw new NotFoundException('Contrato não encontrado');
    return c;
  }

  async activate(tenantId: string, id: string): Promise<Contract> {
    const contract = await this.findOne(tenantId, id);
    if (contract.status !== 'pending_signature' && contract.status !== 'draft')
      throw new BadRequestException('Contrato não pode ser ativado no status atual');
    contract.status = 'active';
    return this.contractRepo.save(contract);
  }

  async linkAutentique(tenantId: string, id: string, docId: string, docUrl?: string): Promise<void> {
    await this.contractRepo.update(
      { id, tenantId },
      { autentiqueDocId: docId, autentiqueDocUrl: docUrl ?? undefined, autentiqueStatus: 'pending', status: 'pending_signature' },
    );
  }

  // Reverte um contrato de 'pending_signature' pra 'draft' quando o envio
  // para assinatura é cancelado — não mexe se já saiu desse estado por
  // outro motivo (ex: já foi ativado).
  async revertToDraft(tenantId: string, id: string): Promise<void> {
    await this.contractRepo.update(
      { id, tenantId, status: 'pending_signature' },
      { status: 'draft', autentiqueDocId: null, autentiqueDocUrl: null, autentiqueStatus: null },
    );
  }

  async markSigned(tenantId: string, id: string, docUrl?: string): Promise<Contract> {
    const contract = await this.findOne(tenantId, id);
    contract.autentiqueStatus = 'signed';
    contract.signedAt = new Date();
    if (docUrl) contract.autentiqueDocUrl = docUrl;
    // Se trigger = on_sign, muda status direto
    if (['draft', 'pending_signature'].includes(contract.status))
      contract.status = 'active';
    return this.contractRepo.save(contract);
  }

  async cancel(tenantId: string, id: string, dto: CancelContractDto): Promise<Contract> {
    const contract = await this.findOne(tenantId, id);
    if (['cancelled', 'terminated', 'finished'].includes(contract.status))
      throw new BadRequestException('Contrato já encerrado');
    contract.status = 'cancelled';
    contract.cancelledAt = new Date();
    contract.cancellationReason = dto.reason;
    return this.contractRepo.save(contract);
  }

  async terminate(tenantId: string, id: string, dto: TerminateContractDto): Promise<Contract> {
    const contract = await this.findOne(tenantId, id);
    if (contract.status !== 'active')
      throw new BadRequestException('O distrato só pode ser realizado em contratos ativos');
    contract.status = 'terminated';
    contract.cancelledAt = dto.terminationDate
      ? new Date(dto.terminationDate + 'T00:00:00')
      : new Date();
    contract.cancellationReason = dto.reason;
    return this.contractRepo.save(contract);
  }

  // ------------------------------------------------------------------
  // PDF generation
  // ------------------------------------------------------------------
  async generatePdf(tenantId: string, contractId: string): Promise<{ pdfBase64: string; filename: string }> {
    const contract = await this.findOne(tenantId, contractId);

    const [client] = await this.dataSource.query(
      'SELECT * FROM clients WHERE id = $1 AND tenant_id = $2',
      [contract.clientId, tenantId],
    );
    const [tenant] = await this.dataSource.query(
      'SELECT * FROM tenants WHERE id = $1',
      [tenantId],
    );

    let template = '';
    for (const item of contract.items ?? []) {
      if (item.productId) {
        const [prod] = await this.dataSource.query(
          'SELECT contract_template FROM products WHERE id = $1',
          [item.productId],
        );
        if (prod?.contract_template) { template = prod.contract_template; break; }
      }
    }
    if (!template) {
      throw new BadRequestException(
        'Nenhum template de contrato configurado. Acesse Produtos e defina o template no produto.',
      );
    }

    const text = this.replaceMnemonics(template, contract, client, tenant);
    const pdfBase64 = await this.buildPdf(text, tenant?.letterhead_pdf ?? null);
    const filename = `Contrato_${contract.number.replace(/\//g, '-')}.pdf`;
    return { pdfBase64, filename };
  }

  private replaceMnemonics(template: string, contract: Contract, client: any, tenant: any): string {
    const fmt = (v: number) =>
      new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(v));
    const fmtDate = (d: string | null) =>
      d ? new Date(d + 'T00:00:00').toLocaleDateString('pt-BR') : '—';
    const extenso = (v: number) => valorPorExtenso(v);
    const today = new Date().toLocaleDateString('pt-BR');

    const addressParts = [
      client?.address,
      client?.address_number,
      client?.neighborhood,
      client?.city,
      client?.uf ? `- ${client.uf}` : null,
      client?.zip_code,
    ].filter(Boolean);
    const address = addressParts.join(', ');

    // Contract duration
    let duration = 'indeterminado';
    if (contract.startDate && contract.endDate) {
      const start = new Date(contract.startDate + 'T00:00:00');
      const end = new Date(contract.endDate + 'T00:00:00');
      const months = (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth());
      duration = `${months} ${months === 1 ? 'mês' : 'meses'}`;
    }

    const BILLING_LABEL: Record<string, string> = {
      monthly: 'Mensal', yearly: 'Anual', once: 'Avulso', hourly: 'Por uso',
    };
    const BILLING_ADVERB: Record<string, string> = {
      monthly: 'mensalmente', yearly: 'anualmente', once: 'por prestação única', hourly: 'por hora utilizada',
    };

    const firstItem = contract.items?.[0];
    const cycle = firstItem?.billingCycle ?? '';

    return template
      // ── Mnemônicos ANEXO I (inglês) ────────────────────────────────
      .replace(/\{\{CONTRACT_NUMBER\}\}/g, contract.number ?? '')
      .replace(/\{\{ISSUE_DATE\}\}/g, today)
      .replace(/\{\{CLIENT_NAME\}\}/g, client?.name ?? '')
      .replace(/\{\{CLIENT_DOCUMENT\}\}/g, client?.document ?? '')
      .replace(/\{\{CLIENT_ADDRESS\}\}/g, address)
      .replace(/\{\{CLIENT_EMAIL\}\}/g, client?.email_commercial ?? client?.email ?? '')
      .replace(/\{\{CONTRACT_DURATION\}\}/g, duration)
      .replace(/\{\{PRODUCT_NAME\}\}/g, firstItem?.name ?? '')
      .replace(/\{\{BILLING_TYPE\}\}/g, BILLING_LABEL[cycle] ?? cycle)
      .replace(/\{\{QUANTITY\}\}/g, firstItem ? String(Number(firstItem.quantity)) : '')
      .replace(/\{\{UNIT_PRICE\}\}/g, firstItem ? fmt(firstItem.unitPrice) : '')
      .replace(/\{\{TOTAL_PRICE\}\}/g, firstItem ? fmt(firstItem.total) : '')
      .replace(/\{\{LICENSE_TOTAL\}\}/g, fmt(contract.total))
      .replace(/\{\{BILLING_CYCLE\}\}/g, BILLING_ADVERB[cycle] ?? cycle)
      .replace(/\{\{ADJUSTMENT_INDEX\}\}/g, contract.adjustmentIndex ?? 'IPCA')
      // ── Mnemônicos legados (português) ─ retrocompatibilidade ──────
      .replace(/\{\{NOME_CLIENTE\}\}/g, client?.name ?? '')
      .replace(/\{\{CNPJ_CLIENTE\}\}/g, client?.document ?? '')
      .replace(/\{\{CPF_CLIENTE\}\}/g, client?.document ?? '')
      .replace(/\{\{EMAIL_CLIENTE\}\}/g, client?.email_commercial ?? client?.email ?? '')
      .replace(/\{\{TELEFONE_CLIENTE\}\}/g, client?.phone_commercial ?? client?.phone ?? '')
      .replace(/\{\{ENDERECO_CLIENTE\}\}/g, address)
      .replace(/\{\{NOME_PRESTADOR\}\}/g, tenant?.name ?? '')
      .replace(/\{\{CNPJ_PRESTADOR\}\}/g, tenant?.document ?? '')
      .replace(/\{\{NUMERO_CONTRATO\}\}/g, contract.number ?? '')
      .replace(/\{\{DATA_INICIO\}\}/g, fmtDate(contract.startDate))
      .replace(/\{\{DATA_FIM\}\}/g, fmtDate(contract.endDate))
      .replace(/\{\{VALOR_TOTAL\}\}/g, fmt(contract.total))
      .replace(/\{\{VALOR_TOTAL_EXTENSO\}\}/g, extenso(contract.total))
      .replace(/\{\{NUM_PARCELAS\}\}/g, contract.installmentsCount ? String(contract.installmentsCount) : '')
      .replace(/\{\{VALOR_PARCELA\}\}/g, contract.installmentAmount ? fmt(contract.installmentAmount) : '')
      .replace(/\{\{DATA_PRIMEIRA_PARCELA\}\}/g, contract.firstDueDate ? fmtDate(contract.firstDueDate) : '')
      .replace(/\{\{DIA_VENCIMENTO\}\}/g, String(contract.billingDay ?? ''))
      .replace(/\{\{PRAZO_AVISO\}\}/g, String(contract.noticeDays ?? ''))
      .replace(/\{\{DATA_HOJE\}\}/g, today)
      .replace(/\{\{LISTA_ITENS\}\}/g,
        (contract.items ?? []).map((i) => `• ${i.name}: ${fmt(i.unitPrice)} × ${i.quantity} = ${fmt(i.total)}`).join('\n'),
      )
      .replace(/\{\{OBSERVACOES\}\}/g, contract.notes ?? '');
  }

  private async buildPdf(text: string, letterheadBase64: string | null): Promise<string> {
    const { PDFDocument, StandardFonts, rgb } = await import('pdf-lib');

    const pdfDoc = await PDFDocument.create();
    const font = await pdfDoc.embedFont(StandardFonts.Helvetica);

    const pageWidth = 595.28;   // A4
    const pageHeight = 841.89;
    const marginLeft = 65;
    const marginRight = 65;
    const marginTop = letterheadBase64 ? 160 : 70;
    const marginBottom = letterheadBase64 ? 80 : 70;
    const fontSize = 11;
    const lineHeight = 17;
    const textWidth = pageWidth - marginLeft - marginRight;

    let letterheadEmbedded: any = null;
    if (letterheadBase64) {
      try {
        const bytes = Buffer.from(letterheadBase64, 'base64');
        const embedded = await pdfDoc.embedPdf(bytes, [0]);
        letterheadEmbedded = embedded[0];
      } catch {
        letterheadEmbedded = null;
      }
    }

    // Wrap text
    const rawLines = text.split('\n');
    const wrappedLines: string[] = [];
    for (const raw of rawLines) {
      if (!raw.trim()) { wrappedLines.push(''); continue; }
      const words = raw.split(' ');
      let cur = '';
      for (const word of words) {
        const test = cur ? `${cur} ${word}` : word;
        if (font.widthOfTextAtSize(test, fontSize) > textWidth && cur) {
          wrappedLines.push(cur); cur = word;
        } else { cur = test; }
      }
      if (cur) wrappedLines.push(cur);
    }

    // Paginate
    const linesPerPage = Math.floor((pageHeight - marginTop - marginBottom) / lineHeight);
    const pages: string[][] = [];
    for (let i = 0; i < wrappedLines.length; i += linesPerPage) {
      pages.push(wrappedLines.slice(i, i + linesPerPage));
    }
    if (!pages.length) pages.push(['']);

    for (const pageLines of pages) {
      const page = pdfDoc.addPage([pageWidth, pageHeight]);
      if (letterheadEmbedded) {
        page.drawPage(letterheadEmbedded, { x: 0, y: 0, width: pageWidth, height: pageHeight });
      }
      for (let i = 0; i < pageLines.length; i++) {
        if (pageLines[i]) {
          page.drawText(pageLines[i], {
            x: marginLeft,
            y: pageHeight - marginTop - i * lineHeight,
            size: fontSize,
            font,
            color: rgb(0, 0, 0),
          });
        }
      }
    }

    const bytes = await pdfDoc.save();
    return Buffer.from(bytes).toString('base64');
  }
}

// ============================================================
// CONTROLLER
// ============================================================
@ApiTags('Contratos')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Controller('contracts')
export class ContractsController {
  constructor(private readonly contractsService: ContractsService) {}

  @Post('from-proposal/:proposalId')
  @ApiOperation({ summary: 'Gerar contrato a partir de proposta aceita' })
  fromProposal(
    @Param('proposalId') proposalId: string,
    @Body() dto: CreateContractDto,
    @Request() req: any,
  ) {
    return this.contractsService.createFromProposal(req.user.tenantId, req.user.userId, proposalId, dto);
  }

  @Get()
  @ApiOperation({ summary: 'Listar contratos' })
  findAll(
    @Request() req: any,
    @Query('status') status?: string,
    @Query('clientId') clientId?: string,
  ) {
    return this.contractsService.findAll(req.user.tenantId, status, clientId);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Detalhar contrato' })
  findOne(@Param('id') id: string, @Request() req: any) {
    return this.contractsService.findOne(req.user.tenantId, id);
  }

  @Patch(':id/activate')
  @ApiOperation({ summary: 'Ativar contrato' })
  activate(@Param('id') id: string, @Request() req: any) {
    return this.contractsService.activate(req.user.tenantId, id);
  }

  @Patch(':id/signed')
  @ApiOperation({ summary: 'Registrar assinatura do contrato' })
  markSigned(@Param('id') id: string, @Request() req: any) {
    return this.contractsService.markSigned(req.user.tenantId, id);
  }

  @Patch(':id/cancel')
  @ApiOperation({ summary: 'Cancelar contrato' })
  cancel(@Param('id') id: string, @Body() dto: CancelContractDto, @Request() req: any) {
    return this.contractsService.cancel(req.user.tenantId, id, dto);
  }

  @Patch(':id/terminate')
  @ApiOperation({ summary: 'Realizar distrato do contrato ativo' })
  terminate(@Param('id') id: string, @Body() dto: TerminateContractDto, @Request() req: any) {
    return this.contractsService.terminate(req.user.tenantId, id, dto);
  }

  @Post(':id/generate-pdf')
  @ApiOperation({ summary: 'Gerar PDF do contrato com template e timbre' })
  generatePdf(@Param('id') id: string, @Request() req: any) {
    return this.contractsService.generatePdf(req.user.tenantId, id);
  }
}

// ============================================================
// MODULE
// ============================================================
@Module({
  imports: [TypeOrmModule.forFeature([Contract, ContractItem])],
  controllers: [ContractsController],
  providers: [ContractsService],
  exports: [ContractsService],
})
export class ContractsModule {}
