import { Repository } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import {
  PaymentTerm,
  PaymentTermLine,
  PaymentTermLineType,
} from './entities/payment-term.entity.js';
import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ============================================================================
// TYPES
// ============================================================================

export interface CreatePaymentTermLineDto {
  sequence?: number;
  line_type?: PaymentTermLineType | 'balance' | 'percent' | 'fixed';
  lineType?: PaymentTermLineType | 'balance' | 'percent' | 'fixed';
  value_percent?: number;
  valuePercent?: number;
  value_amount?: number;
  valueAmount?: number;
  days?: number;
  day_of_month?: number;
  dayOfMonth?: number;
  end_of_month?: boolean;
  endOfMonth?: boolean;
}

export interface CreatePaymentTermDto {
  code: string;
  name: string;
  description?: string;
  due_days?: number;
  dueDays?: number;
  discount_percent?: number;
  discountPercent?: number;
  discount_days?: number;
  discountDays?: number;
  is_immediate?: boolean;
  isImmediate?: boolean;
  lines?: CreatePaymentTermLineDto[];
}

export interface UpdatePaymentTermDto {
  name?: string;
  description?: string | null;
  due_days?: number;
  dueDays?: number;
  discount_percent?: number | null;
  discountPercent?: number | null;
  discount_days?: number | null;
  discountDays?: number | null;
  is_immediate?: boolean;
  isImmediate?: boolean;
  is_active?: boolean;
  isActive?: boolean;
  lines?: CreatePaymentTermLineDto[];
}

export interface DueDateResult {
  dueDate: Date;
  discountDate: Date | null;
  discountAmount: number;
  lines: Array<{
    dueDate: Date;
    amount: number;
    percent: number;
  }>;
}

// ============================================================================
// SERVICE
// ============================================================================

class PaymentTermsService {
  private repository: Repository<PaymentTerm>;
  private lineRepository: Repository<PaymentTermLine>;

  constructor() {
    this.repository = AppDataSource.getRepository(PaymentTerm);
    this.lineRepository = AppDataSource.getRepository(PaymentTermLine);
  }

  /**
   * Calculate due date(s) based on payment term
   */
  calculateDueDate(
    paymentTerm: PaymentTerm,
    invoiceDate: Date,
    totalAmount: number
  ): DueDateResult {
    logger.debug('Calculating due date', {
      termCode: paymentTerm.code,
      invoiceDate,
      totalAmount,
    });

    const baseDate = new Date(invoiceDate);
    const lines: DueDateResult['lines'] = [];

    // If immediate payment
    if (paymentTerm.isImmediate) {
      return {
        dueDate: baseDate,
        discountDate: null,
        discountAmount: 0,
        lines: [{ dueDate: baseDate, amount: totalAmount, percent: 100 }],
      };
    }

    // If payment term has lines, use them
    if (paymentTerm.lines && paymentTerm.lines.length > 0) {
      let remainingAmount = totalAmount;
      let lastDueDate = baseDate;

      for (const line of paymentTerm.lines.sort((a, b) => a.sequence - b.sequence)) {
        let lineAmount = 0;
        let linePercent = 0;

        if (line.lineType === PaymentTermLineType.BALANCE) {
          lineAmount = remainingAmount;
          linePercent = (lineAmount / totalAmount) * 100;
        } else if (line.lineType === PaymentTermLineType.PERCENT && line.valuePercent) {
          linePercent = Number(line.valuePercent);
          lineAmount = (totalAmount * linePercent) / 100;
        } else if (line.lineType === PaymentTermLineType.FIXED && line.valueAmount) {
          lineAmount = Math.min(Number(line.valueAmount), remainingAmount);
          linePercent = (lineAmount / totalAmount) * 100;
        }

        const lineDueDate = this.calculateLineDueDate(baseDate, line);
        lastDueDate = lineDueDate;

        lines.push({
          dueDate: lineDueDate,
          amount: lineAmount,
          percent: linePercent,
        });

        remainingAmount -= lineAmount;
      }

      // Calculate discount date if applicable
      let discountDate: Date | null = null;
      let discountAmount = 0;

      if (paymentTerm.discountPercent && paymentTerm.discountDays) {
        discountDate = new Date(baseDate);
        discountDate.setDate(discountDate.getDate() + paymentTerm.discountDays);
        discountAmount = (totalAmount * Number(paymentTerm.discountPercent)) / 100;
      }

      return {
        dueDate: lastDueDate,
        discountDate,
        discountAmount,
        lines,
      };
    }

    // Simple due days calculation
    const dueDate = new Date(baseDate);
    dueDate.setDate(dueDate.getDate() + paymentTerm.dueDays);

    let discountDate: Date | null = null;
    let discountAmount = 0;

    if (paymentTerm.discountPercent && paymentTerm.discountDays) {
      discountDate = new Date(baseDate);
      discountDate.setDate(discountDate.getDate() + paymentTerm.discountDays);
      discountAmount = (totalAmount * Number(paymentTerm.discountPercent)) / 100;
    }

    return {
      dueDate,
      discountDate,
      discountAmount,
      lines: [{ dueDate, amount: totalAmount, percent: 100 }],
    };
  }

  /**
   * Calculate due date for a specific line
   */
  private calculateLineDueDate(baseDate: Date, line: PaymentTermLine): Date {
    const result = new Date(baseDate);
    result.setDate(result.getDate() + line.days);

    // If specific day of month
    if (line.dayOfMonth) {
      result.setDate(line.dayOfMonth);
      // If the calculated date is before base + days, move to next month
      const minDate = new Date(baseDate);
      minDate.setDate(minDate.getDate() + line.days);
      if (result < minDate) {
        result.setMonth(result.getMonth() + 1);
      }
    }

    // If end of month
    if (line.endOfMonth) {
      result.setMonth(result.getMonth() + 1);
      result.setDate(0); // Last day of previous month
    }

    return result;
  }

  /**
   * Get all payment terms for a tenant
   */
  async findAll(tenantId: string, activeOnly: boolean = false): Promise<PaymentTerm[]> {
    logger.debug('Finding all payment terms', { tenantId, activeOnly });

    const query = this.repository
      .createQueryBuilder('pt')
      .leftJoinAndSelect('pt.lines', 'lines')
      .where('pt.tenant_id = :tenantId', { tenantId })
      .orderBy('pt.sequence', 'ASC')
      .addOrderBy('pt.name', 'ASC');

    if (activeOnly) {
      query.andWhere('pt.is_active = :isActive', { isActive: true });
    }

    return query.getMany();
  }

  /**
   * Get a specific payment term by ID
   */
  async findById(id: string, tenantId: string): Promise<PaymentTerm> {
    logger.debug('Finding payment term by id', { id, tenantId });

    const paymentTerm = await this.repository.findOne({
      where: { id, tenantId },
      relations: ['lines'],
    });

    if (!paymentTerm) {
      throw new NotFoundError('Término de pago no encontrado');
    }

    return paymentTerm;
  }

  /**
   * Get a specific payment term by code
   */
  async findByCode(code: string, tenantId: string): Promise<PaymentTerm | null> {
    logger.debug('Finding payment term by code', { code, tenantId });

    return this.repository.findOne({
      where: { code, tenantId },
      relations: ['lines'],
    });
  }

  /**
   * Create a new payment term
   */
  async create(
    dto: CreatePaymentTermDto,
    tenantId: string,
    userId?: string
  ): Promise<PaymentTerm> {
    logger.debug('Creating payment term', { dto, tenantId });

    // Check for existing
    const existing = await this.findByCode(dto.code, tenantId);
    if (existing) {
      throw new ConflictError(`Ya existe un término de pago con código ${dto.code}`);
    }

    // Normalize inputs (accept both snake_case and camelCase)
    const dueDays = dto.due_days ?? dto.dueDays ?? 0;
    const discountPercent = dto.discount_percent ?? dto.discountPercent ?? null;
    const discountDays = dto.discount_days ?? dto.discountDays ?? null;
    const isImmediate = dto.is_immediate ?? dto.isImmediate ?? false;

    const paymentTerm = this.repository.create({
      tenantId,
      code: dto.code,
      name: dto.name,
      description: dto.description || null,
      dueDays,
      discountPercent,
      discountDays,
      isImmediate,
      createdBy: userId || null,
    });

    const saved = await this.repository.save(paymentTerm);

    // Create lines if provided
    if (dto.lines && dto.lines.length > 0) {
      await this.createLines(saved.id, dto.lines);
      // Reload with lines
      return this.findById(saved.id, tenantId);
    }

    logger.info('Payment term created', { id: saved.id, code: dto.code, tenantId });

    return saved;
  }

  /**
   * Create payment term lines
   */
  private async createLines(
    paymentTermId: string,
    lines: CreatePaymentTermLineDto[]
  ): Promise<void> {
    for (let index = 0; index < lines.length; index++) {
      const line = lines[index];
      const lineTypeRaw = line.line_type ?? line.lineType ?? 'balance';
      const lineType = lineTypeRaw as PaymentTermLineType;
      const valuePercent = line.value_percent ?? line.valuePercent ?? null;
      const valueAmount = line.value_amount ?? line.valueAmount ?? null;
      const dayOfMonth = line.day_of_month ?? line.dayOfMonth ?? null;
      const endOfMonth = line.end_of_month ?? line.endOfMonth ?? false;

      const lineEntity = this.lineRepository.create({
        paymentTermId,
        sequence: line.sequence ?? index + 1,
        lineType,
        valuePercent,
        valueAmount,
        days: line.days ?? 0,
        dayOfMonth,
        endOfMonth,
      });
      await this.lineRepository.save(lineEntity);
    }
  }

  /**
   * Update a payment term
   */
  async update(
    id: string,
    dto: UpdatePaymentTermDto,
    tenantId: string,
    userId?: string
  ): Promise<PaymentTerm> {
    logger.debug('Updating payment term', { id, dto, tenantId });

    const existing = await this.findById(id, tenantId);

    // Normalize inputs
    const dueDays = dto.due_days ?? dto.dueDays;
    const discountPercent = dto.discount_percent ?? dto.discountPercent;
    const discountDays = dto.discount_days ?? dto.discountDays;
    const isImmediate = dto.is_immediate ?? dto.isImmediate;
    const isActive = dto.is_active ?? dto.isActive;

    if (dto.name !== undefined) {
      existing.name = dto.name;
    }
    if (dto.description !== undefined) {
      existing.description = dto.description;
    }
    if (dueDays !== undefined) {
      existing.dueDays = dueDays;
    }
    if (discountPercent !== undefined) {
      existing.discountPercent = discountPercent;
    }
    if (discountDays !== undefined) {
      existing.discountDays = discountDays;
    }
    if (isImmediate !== undefined) {
      existing.isImmediate = isImmediate;
    }
    if (isActive !== undefined) {
      existing.isActive = isActive;
    }

    existing.updatedBy = userId || null;

    const updated = await this.repository.save(existing);

    // Update lines if provided
    if (dto.lines !== undefined) {
      // Remove existing lines
      await this.lineRepository.delete({ paymentTermId: id });
      // Create new lines
      if (dto.lines.length > 0) {
        await this.createLines(id, dto.lines);
      }
    }

    logger.info('Payment term updated', { id, tenantId });

    return this.findById(id, tenantId);
  }

  /**
   * Soft delete a payment term
   */
  async delete(id: string, tenantId: string, userId?: string): Promise<void> {
    logger.debug('Deleting payment term', { id, tenantId });

    const existing = await this.findById(id, tenantId);

    existing.deletedAt = new Date();
    existing.deletedBy = userId || null;

    await this.repository.save(existing);

    logger.info('Payment term deleted', { id, tenantId });
  }

  /**
   * Get common/standard payment terms
   */
  getStandardTerms(): Array<{ code: string; name: string; dueDays: number; discountPercent?: number; discountDays?: number }> {
    return [
      { code: 'IMMEDIATE', name: 'Pago Inmediato', dueDays: 0 },
      { code: 'NET15', name: 'Neto 15 días', dueDays: 15 },
      { code: 'NET30', name: 'Neto 30 días', dueDays: 30 },
      { code: 'NET45', name: 'Neto 45 días', dueDays: 45 },
      { code: 'NET60', name: 'Neto 60 días', dueDays: 60 },
      { code: 'NET90', name: 'Neto 90 días', dueDays: 90 },
      { code: '2/10NET30', name: '2% 10 días, Neto 30', dueDays: 30, discountPercent: 2, discountDays: 10 },
      { code: '1/10NET30', name: '1% 10 días, Neto 30', dueDays: 30, discountPercent: 1, discountDays: 10 },
    ];
  }

  /**
   * Initialize standard payment terms for a tenant
   */
  async initializeForTenant(tenantId: string, userId?: string): Promise<void> {
    logger.debug('Initializing payment terms for tenant', { tenantId });

    const standardTerms = this.getStandardTerms();

    for (const term of standardTerms) {
      const existing = await this.findByCode(term.code, tenantId);
      if (!existing) {
        await this.create(
          {
            code: term.code,
            name: term.name,
            dueDays: term.dueDays,
            discountPercent: term.discountPercent,
            discountDays: term.discountDays,
            isImmediate: term.dueDays === 0,
          },
          tenantId,
          userId
        );
      }
    }

    logger.info('Payment terms initialized for tenant', { tenantId, count: standardTerms.length });
  }
}

export const paymentTermsService = new PaymentTermsService();
