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, Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import {
  Controller, Get, Post, Put, Patch, Delete,
  Body, Param, Query, UseGuards, Request,
  HttpCode, HttpStatus, Ip, Headers,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import {
  IsString, IsOptional, IsNumber, IsArray,
  ValidateNested, IsUUID, IsDateString, Min, MinLength,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

// ============================================================
// ENTITIES
// ============================================================
@Entity('proposals')
export class Proposal {
  @PrimaryGeneratedColumn('uuid') id: string;
  @Column({ name: 'tenant_id' }) tenantId: 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: 'valid_until', type: 'date', nullable: true }) validUntil: string;
  @Column({ name: 'sent_at', type: 'timestamptz', nullable: true }) sentAt: Date;
  @Column({ name: 'viewed_at', type: 'timestamptz', nullable: true }) viewedAt: Date;
  @Column({ name: 'accepted_at', type: 'timestamptz', nullable: true }) acceptedAt: Date;
  @Column({ name: 'rejected_at', type: 'timestamptz', nullable: true }) rejectedAt: Date;
  @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: 'accept_ip', nullable: true }) acceptIp: string;
  @Column({ name: 'accept_user_agent', type: 'text', nullable: true }) acceptUserAgent: string;
  @Column({ type: 'text', nullable: true }) notes: string;
  @Column({ name: 'internal_notes', type: 'text', nullable: true }) internalNotes: string;
  // Cobrança
  @Column({ name: 'billing_cycle', nullable: true }) billingCycle: string;      // monthly | yearly | once | hourly
  @Column({ name: 'payment_terms', length: 200, nullable: true }) paymentTerms: string; // "30 dias após aprovação"
  // Parcelamento — replicado para o contrato quando a proposta é aceita.
  @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({ default: 'draft' }) status: string;
  @OneToMany(() => ProposalItem, (i) => i.proposal, { cascade: true, eager: true })
  items: ProposalItem[];
  @CreateDateColumn({ name: 'created_at' }) createdAt: Date;
  @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date;
}

@Entity('proposal_items')
export class ProposalItem {
  @PrimaryGeneratedColumn('uuid') id: string;
  @Column({ name: 'proposal_id' }) proposalId: 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;
  // Tipo de cobrança do item
  @Column({ name: 'billing_type', default: 'fixed' }) billingType: string;          // fixed | measurement
  @Column({ name: 'included_qty', type: 'numeric', precision: 10, scale: 3, nullable: true }) includedQty: number;
  @Column({ name: 'measurement_unit', length: 30, nullable: true }) measurementUnit: string; // horas, GB, chamadas...
  @Column({ name: 'excess_pricing', default: 'negotiated' }) excessPricing: string; // negotiated | table
  @Column({ name: 'sort_order', default: 0 }) sortOrder: number;
  @ManyToOne(() => Proposal, (p) => p.items, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'proposal_id' })
  proposal: Proposal;
  @CreateDateColumn({ name: 'created_at' }) createdAt: Date;
}

// ============================================================
// DTOs
// ============================================================
export class ProposalItemDto {
  @ApiPropertyOptional() @IsOptional() @IsUUID() productId?: string;
  @ApiPropertyOptional() @IsOptional() @IsUUID() productVariantId?: string;
  @ApiProperty() @IsString() @MinLength(2) name: string;
  @ApiPropertyOptional() @IsOptional() @IsString() description?: string;
  @ApiPropertyOptional() @IsOptional() @IsString() billingCycle?: string;
  @ApiProperty() @IsNumber() @Min(0) unitPrice: number;
  @ApiPropertyOptional({ default: 1 }) @IsOptional() @IsNumber() @Min(0) quantity?: number;
  @ApiPropertyOptional({ default: 0 }) @IsOptional() @IsNumber() @Min(0) discount?: number;
  @ApiPropertyOptional() @IsOptional() @IsString() serviceListItem?: string;
  @ApiPropertyOptional() @IsOptional() @IsNumber() issAliquota?: number;
  @ApiPropertyOptional() @IsOptional() @IsString() discrimination?: string;
  @ApiPropertyOptional({ enum: ['fixed', 'measurement'], default: 'fixed' })
  @IsOptional() @IsString() billingType?: string;
  @ApiPropertyOptional({ description: 'Qtd incluída na faixa (para billingType=measurement)' })
  @IsOptional() @IsNumber() @Min(0) includedQty?: number;
  @ApiPropertyOptional({ description: 'Unidade de medição (horas, GB, chamadas...)' })
  @IsOptional() @IsString() measurementUnit?: string;
  @ApiPropertyOptional({ enum: ['negotiated', 'table'], default: 'negotiated', description: 'Preço para excedente' })
  @IsOptional() @IsString() excessPricing?: string;
  @ApiPropertyOptional({ default: 0 }) @IsOptional() @IsNumber() sortOrder?: number;
}

export class CreateProposalDto {
  @ApiProperty() @IsUUID() clientId: string;
  @ApiPropertyOptional() @IsOptional() @IsString() title?: string;
  @ApiPropertyOptional() @IsOptional() @IsDateString() validUntil?: string;
  @ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
  @ApiPropertyOptional() @IsOptional() @IsString() internalNotes?: string;
  @ApiPropertyOptional({ enum: ['monthly','yearly','once','hourly'], description: 'Ciclo de cobrança da proposta' })
  @IsOptional() @IsString() billingCycle?: string;
  @ApiPropertyOptional({ example: '30 dias após aprovação' })
  @IsOptional() @IsString() paymentTerms?: string;
  @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;
  @ApiProperty({ type: [ProposalItemDto] })
  @IsArray() @ValidateNested({ each: true }) @Type(() => ProposalItemDto)
  items: ProposalItemDto[];
}

export class AcceptProposalDto {
  @ApiPropertyOptional({ description: 'Token de aceite enviado por e-mail' })
  @IsOptional() @IsString() token?: string;
}

// ============================================================
// SERVICE
// ============================================================
@Injectable()
export class ProposalsService {
  private readonly logger = new Logger(ProposalsService.name);

  constructor(
    @InjectRepository(Proposal)
    private readonly proposalRepo: Repository<Proposal>,

    @InjectRepository(ProposalItem)
    private readonly itemRepo: Repository<ProposalItem>,

    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, 'proposal', 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 `PROP-${new Date().getFullYear()}-${seq}`;
  }

  private calcTotals(items: ProposalItemDto[]) {
    let subtotal = 0;
    let totalDiscount = 0;
    const mapped = items.map((item) => {
      const qty = item.quantity ?? 1;
      const disc = item.discount ?? 0;
      const total = +(item.unitPrice * qty - disc).toFixed(2);
      subtotal += item.unitPrice * qty;
      totalDiscount += disc;
      return { ...item, quantity: qty, discount: disc, total };
    });
    return { items: mapped, subtotal: +subtotal.toFixed(2), discount: +totalDiscount.toFixed(2), total: +(subtotal - totalDiscount).toFixed(2) };
  }

  async create(tenantId: string, userId: string, dto: CreateProposalDto): Promise<Proposal> {
    const number = await this.nextNumber(tenantId);
    const { items, subtotal, discount, total } = this.calcTotals(dto.items);

    const proposal = this.proposalRepo.create({
      tenantId,
      createdBy: userId,
      clientId: dto.clientId,
      number,
      title: dto.title,
      validUntil: dto.validUntil,
      notes: dto.notes,
      internalNotes: dto.internalNotes,
      billingCycle: dto.billingCycle,
      paymentTerms: dto.paymentTerms,
      installmentsCount: dto.installmentsCount,
      installmentAmount: dto.installmentAmount,
      firstDueDate: dto.firstDueDate,
      subtotal,
      discount,
      total,
      status: 'draft',
      items: items.map((i) => this.itemRepo.create({ ...i, tenantId })),
    });

    return this.proposalRepo.save(proposal);
  }

  async findAll(tenantId: string, status?: string, clientId?: string): Promise<Proposal[]> {
    const qb = this.proposalRepo.createQueryBuilder('p')
      .leftJoinAndSelect('p.items', 'items')
      .where('p.tenant_id = :tenantId', { tenantId })
      .orderBy('p.created_at', 'DESC');
    if (status) qb.andWhere('p.status = :status', { status });
    if (clientId) qb.andWhere('p.client_id = :clientId', { clientId });
    return qb.getMany();
  }

  async findOne(tenantId: string, id: string): Promise<Proposal> {
    const p = await this.proposalRepo.findOne({
      where: { id, tenantId }, relations: ['items'],
    });
    if (!p) throw new NotFoundException('Proposta não encontrada');
    return p;
  }

  async send(tenantId: string, id: string): Promise<Proposal> {
    const proposal = await this.findOne(tenantId, id);
    if (!['draft', 'rejected'].includes(proposal.status))
      throw new BadRequestException('Apenas propostas em rascunho podem ser enviadas');
    proposal.status = 'sent';
    proposal.sentAt = new Date();
    return this.proposalRepo.save(proposal);
  }

  async registerView(tenantId: string, id: string): Promise<void> {
    await this.proposalRepo.update({ id, tenantId }, {
      viewedAt: new Date(),
      status: 'viewed',
    } as any);
  }

  async getPublic(tenantId: string, id: string): Promise<any> {
    const rows = await this.dataSource.query(
      `SELECT p.*,
              t.name AS tenant_name, t.logo_url,
              c.name AS client_name, c.document AS client_document,
              json_agg(pi.* ORDER BY pi.sort_order) AS items
       FROM proposals p
       JOIN tenants t ON t.id = p.tenant_id
       JOIN clients c ON c.id = p.client_id
       LEFT JOIN proposal_items pi ON pi.proposal_id = p.id
       WHERE p.id = $1 AND p.tenant_id = $2
       GROUP BY p.id, t.name, t.logo_url, c.name, c.document`,
      [id, tenantId],
    );
    if (!rows[0]) throw new NotFoundException('Proposta não encontrada');
    const p = rows[0];
    return {
      id: p.id,
      number: p.number,
      title: p.title,
      status: p.status,
      validUntil: p.valid_until,
      notes: p.notes,
      billingCycle: p.billing_cycle,
      paymentTerms: p.payment_terms,
      subtotal: Number(p.subtotal),
      discount: Number(p.discount),
      total: Number(p.total),
      createdAt: p.created_at,
      tenantName: p.tenant_name,
      logoUrl: p.logo_url,
      clientName: p.client_name,
      clientDocument: p.client_document,
      items: (p.items || []).filter(Boolean).map((i: any) => ({
        name: i.name,
        description: i.description,
        billingCycle: i.billing_cycle,
        unitPrice: Number(i.unit_price),
        quantity: Number(i.quantity),
        discount: Number(i.discount),
        total: Number(i.total),
      })),
    };
  }

  private async autoCreateContract(tenantId: string, proposal: Proposal): Promise<void> {
    const seqResult = 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 seqRow = Array.isArray(seqResult[0]) ? seqResult[0][0] : seqResult[0];
    const number = `CONT-${new Date().getFullYear()}-${String(seqRow.last_value).padStart(5, '0')}`;
    const today = new Date().toLocaleDateString('sv-SE');

    const contractResult = await this.dataSource.query(
      `INSERT INTO contracts
         (tenant_id, proposal_id, client_id, created_by, number, title,
          start_date, billing_day, payment_terms, invoice_trigger,
          invoice_days_before, notice_days, subtotal, discount, total, status,
          notes, installments_count, installment_amount, first_due_date)
       VALUES ($1,$2,$3,$4,$5,$6,$7,5,30,'after_payment',0,30,$8,$9,$10,'draft',$11,$12,$13,$14)
       RETURNING id`,
      [tenantId, proposal.id, proposal.clientId, proposal.createdBy, number,
       proposal.title, today, proposal.subtotal, proposal.discount, proposal.total,
       proposal.notes, proposal.installmentsCount, proposal.installmentAmount, proposal.firstDueDate],
    );
    const contractId = contractResult[0].id;

    for (const item of proposal.items ?? []) {
      await this.dataSource.query(
        `INSERT INTO contract_items
           (tenant_id, contract_id, product_id, product_variant_id,
            name, description, billing_cycle, unit_price, quantity, discount, total,
            service_list_item, sort_order)
         VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
        [tenantId, contractId, item.productId ?? null, item.productVariantId ?? null,
         item.name, item.description ?? null, item.billingCycle ?? null,
         item.unitPrice, item.quantity, item.discount, item.total,
         item.serviceListItem ?? null, item.sortOrder ?? 0],
      );
    }
    this.logger.log(`Contrato ${number} criado automaticamente [proposal: ${proposal.id}]`);
  }

  async accept(
    tenantId: string,
    id: string,
    ip: string,
    userAgent: string,
  ): Promise<Proposal> {
    const proposal = await this.findOne(tenantId, id);
    if (!['sent', 'viewed'].includes(proposal.status))
      throw new BadRequestException('Proposta não pode ser aceita no status atual');

    proposal.status = 'accepted';
    proposal.acceptedAt = new Date();
    proposal.acceptIp = ip;
    proposal.acceptUserAgent = userAgent;
    await this.proposalRepo.save(proposal);

    try {
      await this.autoCreateContract(tenantId, proposal);
    } catch (err: any) {
      this.logger.warn(`Falha ao criar contrato automático [proposal: ${id}]: ${err.message}`);
    }

    return proposal;
  }

  async reject(tenantId: string, id: string): Promise<Proposal> {
    const proposal = await this.findOne(tenantId, id);
    proposal.status = 'rejected';
    proposal.rejectedAt = new Date();
    return this.proposalRepo.save(proposal);
  }
}

// ============================================================
// CONTROLLER
// ============================================================
@ApiTags('Propostas')
@ApiBearerAuth()
@Controller('proposals')
export class ProposalsController {
  constructor(private readonly proposalsService: ProposalsService) {}

  @Post()
  @UseGuards(AuthGuard('jwt'))
  @ApiOperation({ summary: 'Criar proposta' })
  create(@Body() dto: CreateProposalDto, @Request() req: any) {
    return this.proposalsService.create(req.user.tenantId, req.user.userId, dto);
  }

  @Get()
  @UseGuards(AuthGuard('jwt'))
  @ApiOperation({ summary: 'Listar propostas' })
  findAll(
    @Request() req: any,
    @Query('status') status?: string,
    @Query('clientId') clientId?: string,
  ) {
    return this.proposalsService.findAll(req.user.tenantId, status, clientId);
  }

  @Get(':id')
  @UseGuards(AuthGuard('jwt'))
  @ApiOperation({ summary: 'Detalhar proposta' })
  findOne(@Param('id') id: string, @Request() req: any) {
    return this.proposalsService.findOne(req.user.tenantId, id);
  }

  @Patch(':id/send')
  @UseGuards(AuthGuard('jwt'))
  @ApiOperation({ summary: 'Marcar proposta como enviada ao cliente' })
  send(@Param('id') id: string, @Request() req: any) {
    return this.proposalsService.send(req.user.tenantId, id);
  }

  @Patch(':id/reject')
  @UseGuards(AuthGuard('jwt'))
  @ApiOperation({ summary: 'Registrar rejeição da proposta' })
  reject(@Param('id') id: string, @Request() req: any) {
    return this.proposalsService.reject(req.user.tenantId, id);
  }

  // Rotas públicas — cliente acessa via link (sem JWT)
  @Get(':tenantId/:id/public')
  @ApiOperation({ summary: 'Dados públicos da proposta para visualização pelo cliente' })
  getPublic(@Param('tenantId') tenantId: string, @Param('id') id: string) {
    return this.proposalsService.getPublic(tenantId, id);
  }

  @Patch(':tenantId/:id/accept')
  @ApiOperation({ summary: 'Aceite digital da proposta pelo cliente (rota pública)' })
  accept(
    @Param('tenantId') tenantId: string,
    @Param('id') id: string,
    @Ip() ip: string,
    @Headers('user-agent') userAgent: string,
  ) {
    return this.proposalsService.accept(tenantId, id, ip, userAgent);
  }

  @Patch(':tenantId/:id/view')
  @ApiOperation({ summary: 'Registrar visualização da proposta pelo cliente' })
  view(@Param('tenantId') tenantId: string, @Param('id') id: string) {
    return this.proposalsService.registerView(tenantId, id);
  }
}

// ============================================================
// MODULE
// ============================================================
@Module({
  imports: [TypeOrmModule.forFeature([Proposal, ProposalItem])],
  controllers: [ProposalsController],
  providers: [ProposalsService],
  exports: [ProposalsService],
})
export class ProposalsModule {}
