/**
 * PaymentTermService - Payment Terms Management
 *
 * Provides centralized payment term operations:
 * - Payment term CRUD
 * - Due date calculations
 * - Multi-line payment schedules
 *
 * @module Core
 */

import { Repository, IsNull } from 'typeorm';
import {
  PaymentTerm,
  PaymentTermLine,
  PaymentTermLineType,
} from '../entities/payment-term.entity';

/**
 * Service context for multi-tenant operations
 */
export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

/**
 * Paginated result interface
 */
export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface CreatePaymentTermLineDto {
  sequence: number;
  lineType: PaymentTermLineType;
  valuePercent?: number;
  valueAmount?: number;
  days: number;
  dayOfMonth?: number;
  endOfMonth?: boolean;
}

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

export interface UpdatePaymentTermDto {
  name?: string;
  description?: string;
  dueDays?: number;
  discountPercent?: number;
  discountDays?: number;
  isImmediate?: boolean;
  isActive?: boolean;
  sequence?: number;
  lines?: CreatePaymentTermLineDto[];
}

export interface PaymentTermFilters {
  code?: string;
  isActive?: boolean;
  isImmediate?: boolean;
  companyId?: string;
  search?: string;
}

export interface PaymentScheduleItem {
  sequence: number;
  dueDate: Date;
  amount: number;
  percent: number;
  lineType: PaymentTermLineType;
}

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

  constructor(
    repository: Repository<PaymentTerm>,
    lineRepository: Repository<PaymentTermLine>
  ) {
    this.repository = repository;
    this.lineRepository = lineRepository;
  }

  /**
   * Create a new payment term
   */
  async create(
    ctx: ServiceContext,
    data: CreatePaymentTermDto
  ): Promise<PaymentTerm> {
    const existing = await this.findByCode(ctx, data.code);
    if (existing) {
      throw new Error(`Payment term with code '${data.code}' already exists`);
    }

    // Create the payment term
    const entity = this.repository.create({
      tenantId: ctx.tenantId,
      companyId: data.companyId ?? null,
      code: data.code,
      name: data.name,
      description: data.description ?? null,
      dueDays: data.dueDays ?? 0,
      discountPercent: data.discountPercent ?? 0,
      discountDays: data.discountDays ?? 0,
      isImmediate: data.isImmediate ?? false,
      isActive: true,
      createdBy: ctx.userId ?? null,
    });

    const savedEntity = await this.repository.save(entity);

    // Create lines if provided
    if (data.lines && data.lines.length > 0) {
      const lines = data.lines.map((line) =>
        this.lineRepository.create({
          paymentTermId: savedEntity.id,
          sequence: line.sequence,
          lineType: line.lineType,
          valuePercent: line.valuePercent ?? null,
          valueAmount: line.valueAmount ?? null,
          days: line.days,
          dayOfMonth: line.dayOfMonth ?? null,
          endOfMonth: line.endOfMonth ?? false,
        })
      );
      await this.lineRepository.save(lines);
    }

    return this.findById(ctx, savedEntity.id) as Promise<PaymentTerm>;
  }

  /**
   * Find payment term by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<PaymentTerm | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['lines'],
    });
  }

  /**
   * Find payment term by code
   */
  async findByCode(
    ctx: ServiceContext,
    code: string
  ): Promise<PaymentTerm | null> {
    return this.repository.findOne({
      where: { tenantId: ctx.tenantId, code, deletedAt: IsNull() },
      relations: ['lines'],
    });
  }

  /**
   * Get all active payment terms
   */
  async findAll(ctx: ServiceContext): Promise<PaymentTerm[]> {
    return this.repository.find({
      where: { tenantId: ctx.tenantId, isActive: true, deletedAt: IsNull() },
      relations: ['lines'],
      order: { sequence: 'ASC', name: 'ASC' },
    });
  }

  /**
   * Find payment terms with filters and pagination
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: PaymentTermFilters,
    page = 1,
    limit = 50
  ): Promise<PaginatedResult<PaymentTerm>> {
    const qb = this.repository
      .createQueryBuilder('pt')
      .leftJoinAndSelect('pt.lines', 'lines')
      .where('pt.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('pt.deleted_at IS NULL');

    if (filters.code) {
      qb.andWhere('pt.code = :code', { code: filters.code });
    }
    if (filters.isActive !== undefined) {
      qb.andWhere('pt.is_active = :isActive', { isActive: filters.isActive });
    }
    if (filters.isImmediate !== undefined) {
      qb.andWhere('pt.is_immediate = :isImmediate', {
        isImmediate: filters.isImmediate,
      });
    }
    if (filters.companyId) {
      qb.andWhere('pt.company_id = :companyId', { companyId: filters.companyId });
    }
    if (filters.search) {
      qb.andWhere('(pt.code ILIKE :search OR pt.name ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('pt.sequence', 'ASC')
      .addOrderBy('pt.name', 'ASC')
      .skip(skip)
      .take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Update payment term
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: UpdatePaymentTermDto
  ): Promise<PaymentTerm | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    // Update main entity
    const { lines, ...termData } = data;
    Object.assign(entity, {
      ...termData,
      updatedBy: ctx.userId ?? null,
    });

    await this.repository.save(entity);

    // Update lines if provided
    if (lines !== undefined) {
      // Delete existing lines
      await this.lineRepository.delete({ paymentTermId: id });

      // Create new lines
      if (lines.length > 0) {
        const newLines = lines.map((line) =>
          this.lineRepository.create({
            paymentTermId: id,
            sequence: line.sequence,
            lineType: line.lineType,
            valuePercent: line.valuePercent ?? null,
            valueAmount: line.valueAmount ?? null,
            days: line.days,
            dayOfMonth: line.dayOfMonth ?? null,
            endOfMonth: line.endOfMonth ?? false,
          })
        );
        await this.lineRepository.save(newLines);
      }
    }

    return this.findById(ctx, id);
  }

  /**
   * Soft delete payment term
   */
  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return false;
    }

    entity.deletedAt = new Date();
    entity.deletedBy = ctx.userId ?? null;
    await this.repository.save(entity);
    return true;
  }

  /**
   * Hard delete payment term
   */
  async hardDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    // Delete lines first
    await this.lineRepository.delete({ paymentTermId: id });

    const result = await this.repository.delete({ id, tenantId: ctx.tenantId });
    return (result.affected || 0) > 0;
  }

  // ==================== CALCULATION OPERATIONS ====================

  /**
   * Calculate due date based on payment term
   */
  async calculateDueDate(
    ctx: ServiceContext,
    paymentTermId: string,
    invoiceDate: Date
  ): Promise<Date> {
    const term = await this.findById(ctx, paymentTermId);
    if (!term) {
      throw new Error('Payment term not found');
    }

    if (term.isImmediate) {
      return new Date(invoiceDate);
    }

    const dueDate = new Date(invoiceDate);
    dueDate.setDate(dueDate.getDate() + term.dueDays);

    return dueDate;
  }

  /**
   * Calculate early payment discount deadline
   */
  async calculateDiscountDate(
    ctx: ServiceContext,
    paymentTermId: string,
    invoiceDate: Date
  ): Promise<{ date: Date; discountPercent: number } | null> {
    const term = await this.findById(ctx, paymentTermId);
    if (!term) {
      throw new Error('Payment term not found');
    }

    if (!term.discountDays || !term.discountPercent) {
      return null;
    }

    const discountDate = new Date(invoiceDate);
    discountDate.setDate(discountDate.getDate() + term.discountDays);

    return {
      date: discountDate,
      discountPercent: Number(term.discountPercent),
    };
  }

  /**
   * Generate payment schedule for multi-line payment terms
   */
  async generatePaymentSchedule(
    ctx: ServiceContext,
    paymentTermId: string,
    invoiceDate: Date,
    totalAmount: number
  ): Promise<PaymentScheduleItem[]> {
    const term = await this.findById(ctx, paymentTermId);
    if (!term) {
      throw new Error('Payment term not found');
    }

    // If no lines, use simple due date
    if (!term.lines || term.lines.length === 0) {
      const dueDate = await this.calculateDueDate(ctx, paymentTermId, invoiceDate);
      return [
        {
          sequence: 1,
          dueDate,
          amount: totalAmount,
          percent: 100,
          lineType: PaymentTermLineType.BALANCE,
        },
      ];
    }

    // Sort lines by sequence
    const sortedLines = [...term.lines].sort((a, b) => a.sequence - b.sequence);

    const schedule: PaymentScheduleItem[] = [];
    let remainingAmount = totalAmount;

    for (const line of sortedLines) {
      let lineAmount: number;
      let linePercent: number;

      switch (line.lineType) {
        case PaymentTermLineType.PERCENT:
          linePercent = Number(line.valuePercent) || 0;
          lineAmount = (totalAmount * linePercent) / 100;
          break;
        case PaymentTermLineType.FIXED:
          lineAmount = Math.min(Number(line.valueAmount) || 0, remainingAmount);
          linePercent = (lineAmount / totalAmount) * 100;
          break;
        case PaymentTermLineType.BALANCE:
        default:
          lineAmount = remainingAmount;
          linePercent = (lineAmount / totalAmount) * 100;
          break;
      }

      // Calculate due date for this line
      let dueDate = new Date(invoiceDate);
      dueDate.setDate(dueDate.getDate() + line.days);

      // Apply day of month if specified
      if (line.dayOfMonth) {
        dueDate.setDate(line.dayOfMonth);
        // If day already passed this month, move to next month
        if (dueDate <= invoiceDate) {
          dueDate.setMonth(dueDate.getMonth() + 1);
        }
      }

      // Apply end of month if specified
      if (line.endOfMonth) {
        dueDate = new Date(dueDate.getFullYear(), dueDate.getMonth() + 1, 0);
      }

      // Round amounts to 2 decimals
      lineAmount = Math.round(lineAmount * 100) / 100;

      schedule.push({
        sequence: line.sequence,
        dueDate,
        amount: lineAmount,
        percent: Math.round(linePercent * 100) / 100,
        lineType: line.lineType,
      });

      remainingAmount -= lineAmount;
    }

    return schedule;
  }

  /**
   * Check if payment is overdue
   */
  isOverdue(dueDate: Date): boolean {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const due = new Date(dueDate);
    due.setHours(0, 0, 0, 0);
    return today > due;
  }

  /**
   * Calculate days overdue
   */
  getDaysOverdue(dueDate: Date): number {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const due = new Date(dueDate);
    due.setHours(0, 0, 0, 0);

    const diffTime = today.getTime() - due.getTime();
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

    return Math.max(0, diffDays);
  }
}
