/**
 * AccountingService - Servicio de Contabilidad
 *
 * Gestión del catálogo de cuentas y pólizas contables.
 *
 * @module Finance
 */

import { DataSource, Repository, IsNull, Not, In } from 'typeorm';
import {
  ChartOfAccounts,
  AccountType,
  AccountNature,
  AccountStatus,
  AccountingEntry,
  EntryType,
  EntryStatus,
  AccountingEntryLine,
} from '../entities';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

interface PaginatedResult<T> {
  data: T[];
  meta: {
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  };
}

interface CreateAccountDto {
  code: string;
  name: string;
  accountType: AccountType;
  nature: AccountNature;
  parentId?: string;
  description?: string;
  level?: number;
  isGroupAccount?: boolean;
  acceptsMovements?: boolean;
  satCode?: string;
  satDescription?: string;
  currencyCode?: string;
  metadata?: Record<string, any>;
}

interface UpdateAccountDto {
  name?: string;
  description?: string;
  status?: AccountStatus;
  acceptsMovements?: boolean;
  satCode?: string;
  satDescription?: string;
  metadata?: Record<string, any>;
}

interface CreateEntryDto {
  entryType: EntryType;
  entryDate: Date;
  reference?: string;
  description: string;
  projectId?: string;
  projectCode?: string;
  costCenterId?: string;
  fiscalYear: number;
  fiscalPeriod: number;
  currencyCode?: string;
  exchangeRate?: number;
  sourceDocument?: string;
  sourceDocumentId?: string;
  notes?: string;
  lines: CreateEntryLineDto[];
}

interface CreateEntryLineDto {
  accountId: string;
  accountCode: string;
  description?: string;
  debitAmount?: number;
  creditAmount?: number;
  currencyAmount?: number;
  currencyCode?: string;
  exchangeRate?: number;
  costCenterId?: string;
  projectId?: string;
  partnerId?: string;
  reference?: string;
  metadata?: Record<string, any>;
}

interface AccountWithChildren extends ChartOfAccounts {
  children?: AccountWithChildren[];
}

export class AccountingService {
  private accountRepository: Repository<ChartOfAccounts>;
  private entryRepository: Repository<AccountingEntry>;
  private lineRepository: Repository<AccountingEntryLine>;

  constructor(private dataSource: DataSource) {
    this.accountRepository = dataSource.getRepository(ChartOfAccounts);
    this.entryRepository = dataSource.getRepository(AccountingEntry);
    this.lineRepository = dataSource.getRepository(AccountingEntryLine);
  }

  // ==================== CATÁLOGO DE CUENTAS ====================

  async findAllAccounts(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      accountType?: AccountType;
      status?: AccountStatus;
      parentId?: string;
      search?: string;
      acceptsMovements?: boolean;
    } = {}
  ): Promise<PaginatedResult<ChartOfAccounts>> {
    const { page = 1, limit = 50, accountType, status, parentId, search, acceptsMovements } = options;

    const queryBuilder = this.accountRepository
      .createQueryBuilder('account')
      .where('account.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('account.deletedAt IS NULL');

    if (accountType) {
      queryBuilder.andWhere('account.accountType = :accountType', { accountType });
    }

    if (status) {
      queryBuilder.andWhere('account.status = :status', { status });
    }

    if (parentId !== undefined) {
      if (parentId === null || parentId === '') {
        queryBuilder.andWhere('account.parentId IS NULL');
      } else {
        queryBuilder.andWhere('account.parentId = :parentId', { parentId });
      }
    }

    if (search) {
      queryBuilder.andWhere(
        '(account.code ILIKE :search OR account.name ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    if (acceptsMovements !== undefined) {
      queryBuilder.andWhere('account.acceptsMovements = :acceptsMovements', { acceptsMovements });
    }

    queryBuilder.orderBy('account.code', 'ASC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

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

  async getAccountTree(ctx: ServiceContext): Promise<AccountWithChildren[]> {
    const accounts = await this.accountRepository.find({
      where: {
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      order: { code: 'ASC' },
    });

    return this.buildTree(accounts, null);
  }

  private buildTree(
    items: ChartOfAccounts[],
    parentId: string | null
  ): AccountWithChildren[] {
    const result: AccountWithChildren[] = [];

    for (const item of items) {
      if (item.parentId === parentId) {
        const children = this.buildTree(items, item.id);
        const node: AccountWithChildren = { ...item };
        if (children.length > 0) {
          node.children = children;
        }
        result.push(node);
      }
    }

    return result;
  }

  async findAccountById(
    ctx: ServiceContext,
    id: string
  ): Promise<ChartOfAccounts | null> {
    return this.accountRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['parent'],
    });
  }

  async findAccountByCode(
    ctx: ServiceContext,
    code: string
  ): Promise<ChartOfAccounts | null> {
    return this.accountRepository.findOne({
      where: {
        code,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  async createAccount(
    ctx: ServiceContext,
    data: CreateAccountDto
  ): Promise<ChartOfAccounts> {
    // Verificar código único
    const existing = await this.findAccountByCode(ctx, data.code);
    if (existing) {
      throw new Error(`Ya existe una cuenta con el código ${data.code}`);
    }

    // Si tiene padre, verificar que existe
    let parentAccount: ChartOfAccounts | null = null;
    if (data.parentId) {
      parentAccount = await this.findAccountById(ctx, data.parentId);
      if (!parentAccount) {
        throw new Error('Cuenta padre no encontrada');
      }
    }

    // Determinar nivel
    const level = data.level ?? (parentAccount ? parentAccount.level + 1 : 1);

    // Generar path completo
    let fullPath = data.code;
    if (parentAccount) {
      fullPath = `${parentAccount.fullPath}/${data.code}`;
    }

    const account = this.accountRepository.create({
      tenantId: ctx.tenantId,
      code: data.code,
      name: data.name,
      description: data.description,
      accountType: data.accountType,
      nature: data.nature,
      level,
      parentId: data.parentId,
      fullPath,
      isGroupAccount: data.isGroupAccount ?? false,
      acceptsMovements: data.acceptsMovements ?? true,
      status: 'active',
      satCode: data.satCode,
      satDescription: data.satDescription,
      currencyCode: data.currencyCode ?? 'MXN',
      balance: 0,
      metadata: data.metadata,
      createdBy: ctx.userId,
    });

    return this.accountRepository.save(account);
  }

  async updateAccount(
    ctx: ServiceContext,
    id: string,
    data: UpdateAccountDto
  ): Promise<ChartOfAccounts> {
    const account = await this.findAccountById(ctx, id);
    if (!account) {
      throw new Error('Cuenta no encontrada');
    }

    Object.assign(account, {
      ...data,
      updatedBy: ctx.userId,
    });

    return this.accountRepository.save(account);
  }

  async deleteAccount(ctx: ServiceContext, id: string): Promise<void> {
    const account = await this.findAccountById(ctx, id);
    if (!account) {
      throw new Error('Cuenta no encontrada');
    }

    // Verificar que no tenga hijos
    const children = await this.accountRepository.count({
      where: {
        tenantId: ctx.tenantId,
        parentId: id,
        deletedAt: IsNull(),
      },
    });

    if (children > 0) {
      throw new Error('No se puede eliminar una cuenta con subcuentas');
    }

    // Verificar que no tenga movimientos
    const movements = await this.lineRepository.count({
      where: {
        accountId: id,
      },
    });

    if (movements > 0) {
      throw new Error('No se puede eliminar una cuenta con movimientos');
    }

    account.deletedAt = new Date();
    account.updatedBy = ctx.userId;
    await this.accountRepository.save(account);
  }

  // ==================== PÓLIZAS CONTABLES ====================

  async findAllEntries(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      entryType?: EntryType;
      status?: EntryStatus;
      startDate?: Date;
      endDate?: Date;
      projectId?: string;
      fiscalYear?: number;
      fiscalPeriod?: number;
      search?: string;
    } = {}
  ): Promise<PaginatedResult<AccountingEntry>> {
    const {
      page = 1,
      limit = 20,
      entryType,
      status,
      startDate,
      endDate,
      projectId,
      fiscalYear,
      fiscalPeriod,
      search,
    } = options;

    const queryBuilder = this.entryRepository
      .createQueryBuilder('entry')
      .leftJoinAndSelect('entry.lines', 'lines')
      .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('entry.deletedAt IS NULL');

    if (entryType) {
      queryBuilder.andWhere('entry.entryType = :entryType', { entryType });
    }

    if (status) {
      queryBuilder.andWhere('entry.status = :status', { status });
    }

    if (startDate) {
      queryBuilder.andWhere('entry.entryDate >= :startDate', { startDate });
    }

    if (endDate) {
      queryBuilder.andWhere('entry.entryDate <= :endDate', { endDate });
    }

    if (projectId) {
      queryBuilder.andWhere('entry.projectId = :projectId', { projectId });
    }

    if (fiscalYear) {
      queryBuilder.andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear });
    }

    if (fiscalPeriod) {
      queryBuilder.andWhere('entry.fiscalPeriod = :fiscalPeriod', { fiscalPeriod });
    }

    if (search) {
      queryBuilder.andWhere(
        '(entry.entryNumber ILIKE :search OR entry.description ILIKE :search OR entry.reference ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    queryBuilder.orderBy('entry.entryDate', 'DESC').addOrderBy('entry.entryNumber', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

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

  async findEntryById(
    ctx: ServiceContext,
    id: string
  ): Promise<AccountingEntry | null> {
    return this.entryRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['lines', 'approvedBy', 'postedBy'],
    });
  }

  async createEntry(
    ctx: ServiceContext,
    data: CreateEntryDto
  ): Promise<AccountingEntry> {
    // Validar que la póliza cuadre
    let totalDebit = 0;
    let totalCredit = 0;

    for (const line of data.lines) {
      totalDebit += line.debitAmount ?? 0;
      totalCredit += line.creditAmount ?? 0;
    }

    if (Math.abs(totalDebit - totalCredit) > 0.01) {
      throw new Error(
        `La póliza no cuadra. Débitos: ${totalDebit}, Créditos: ${totalCredit}`
      );
    }

    // Generar número de póliza
    const entryNumber = await this.generateEntryNumber(
      ctx,
      data.entryType,
      data.fiscalYear,
      data.fiscalPeriod
    );

    // Crear póliza
    const entry = this.entryRepository.create({
      tenantId: ctx.tenantId,
      entryNumber,
      entryType: data.entryType,
      entryDate: data.entryDate,
      reference: data.reference,
      description: data.description,
      projectId: data.projectId,
      projectCode: data.projectCode,
      costCenterId: data.costCenterId,
      fiscalYear: data.fiscalYear,
      fiscalPeriod: data.fiscalPeriod,
      currencyCode: data.currencyCode ?? 'MXN',
      exchangeRate: data.exchangeRate ?? 1,
      totalDebit,
      totalCredit,
      lineCount: data.lines.length,
      sourceDocument: data.sourceDocument,
      sourceDocumentId: data.sourceDocumentId,
      notes: data.notes,
      status: 'draft',
      createdBy: ctx.userId,
    });

    const savedEntry = await this.entryRepository.save(entry);

    // Crear líneas
    const lines = data.lines.map((lineData, index) =>
      this.lineRepository.create({
        entryId: savedEntry.id,
        lineNumber: index + 1,
        accountId: lineData.accountId,
        accountCode: lineData.accountCode,
        description: lineData.description,
        debitAmount: lineData.debitAmount ?? 0,
        creditAmount: lineData.creditAmount ?? 0,
        currencyAmount: lineData.currencyAmount,
        currencyCode: lineData.currencyCode,
        exchangeRate: lineData.exchangeRate,
        costCenterId: lineData.costCenterId,
        projectId: lineData.projectId,
        partnerId: lineData.partnerId,
        reference: lineData.reference,
        metadata: lineData.metadata,
      })
    );

    await this.lineRepository.save(lines);

    return this.findEntryById(ctx, savedEntry.id) as Promise<AccountingEntry>;
  }

  private async generateEntryNumber(
    ctx: ServiceContext,
    entryType: EntryType,
    fiscalYear: number,
    fiscalPeriod: number
  ): Promise<string> {
    const typePrefix: Record<EntryType, string> = {
      purchase: 'PC',
      sale: 'VT',
      payment: 'PG',
      collection: 'CB',
      payroll: 'NM',
      adjustment: 'AJ',
      depreciation: 'DP',
      transfer: 'TR',
      opening: 'AP',
      closing: 'CI',
    };

    const prefix = typePrefix[entryType] || 'PL';

    const lastEntry = await this.entryRepository
      .createQueryBuilder('entry')
      .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear })
      .andWhere('entry.fiscalPeriod = :fiscalPeriod', { fiscalPeriod })
      .andWhere('entry.entryType = :entryType', { entryType })
      .orderBy('entry.entryNumber', 'DESC')
      .getOne();

    let sequence = 1;
    if (lastEntry) {
      const match = lastEntry.entryNumber.match(/(\d+)$/);
      if (match) {
        sequence = parseInt(match[1], 10) + 1;
      }
    }

    return `${prefix}-${fiscalYear}${String(fiscalPeriod).padStart(2, '0')}-${String(sequence).padStart(5, '0')}`;
  }

  async submitForApproval(ctx: ServiceContext, id: string): Promise<AccountingEntry> {
    const entry = await this.findEntryById(ctx, id);
    if (!entry) {
      throw new Error('Póliza no encontrada');
    }

    if (entry.status !== 'draft') {
      throw new Error('Solo se pueden enviar a aprobación pólizas en borrador');
    }

    entry.status = 'pending_approval';
    entry.updatedBy = ctx.userId;

    return this.entryRepository.save(entry);
  }

  async approveEntry(ctx: ServiceContext, id: string): Promise<AccountingEntry> {
    const entry = await this.findEntryById(ctx, id);
    if (!entry) {
      throw new Error('Póliza no encontrada');
    }

    if (entry.status !== 'pending_approval') {
      throw new Error('Solo se pueden aprobar pólizas pendientes de aprobación');
    }

    entry.status = 'approved';
    entry.approvedById = ctx.userId;
    entry.approvedAt = new Date();
    entry.updatedBy = ctx.userId;

    return this.entryRepository.save(entry);
  }

  async postEntry(ctx: ServiceContext, id: string): Promise<AccountingEntry> {
    const entry = await this.findEntryById(ctx, id);
    if (!entry) {
      throw new Error('Póliza no encontrada');
    }

    if (entry.status !== 'approved') {
      throw new Error('Solo se pueden contabilizar pólizas aprobadas');
    }

    // Actualizar saldos de cuentas
    for (const line of entry.lines || []) {
      await this.updateAccountBalance(line.accountId, line.debitAmount, line.creditAmount);
    }

    entry.status = 'posted';
    entry.postedById = ctx.userId;
    entry.postedAt = new Date();
    entry.updatedBy = ctx.userId;

    return this.entryRepository.save(entry);
  }

  private async updateAccountBalance(
    accountId: string,
    debitAmount: number,
    creditAmount: number
  ): Promise<void> {
    const account = await this.accountRepository.findOne({
      where: { id: accountId },
    });

    if (!account) return;

    // Calcular nuevo saldo según naturaleza de la cuenta
    let newBalance = account.balance;
    if (account.nature === 'debit') {
      newBalance += debitAmount - creditAmount;
    } else {
      newBalance += creditAmount - debitAmount;
    }

    await this.accountRepository.update(accountId, {
      balance: newBalance,
      lastMovementDate: new Date(),
    });
  }

  async cancelEntry(ctx: ServiceContext, id: string, reason: string): Promise<AccountingEntry> {
    const entry = await this.findEntryById(ctx, id);
    if (!entry) {
      throw new Error('Póliza no encontrada');
    }

    if (entry.status === 'cancelled' || entry.status === 'reversed') {
      throw new Error('La póliza ya está cancelada o reversada');
    }

    // Si está contabilizada, reversar saldos
    if (entry.status === 'posted') {
      for (const line of entry.lines || []) {
        await this.updateAccountBalance(line.accountId, -line.debitAmount, -line.creditAmount);
      }
    }

    entry.status = 'cancelled';
    entry.notes = `${entry.notes || ''}\n[CANCELADA]: ${reason}`;
    entry.updatedBy = ctx.userId;

    return this.entryRepository.save(entry);
  }

  async reverseEntry(ctx: ServiceContext, id: string, reason: string): Promise<AccountingEntry> {
    const entry = await this.findEntryById(ctx, id);
    if (!entry) {
      throw new Error('Póliza no encontrada');
    }

    if (entry.status !== 'posted') {
      throw new Error('Solo se pueden reversar pólizas contabilizadas');
    }

    // Crear póliza de reverso
    const reversalData: CreateEntryDto = {
      entryType: entry.entryType,
      entryDate: new Date(),
      reference: `REV-${entry.entryNumber}`,
      description: `Reverso de ${entry.entryNumber}: ${reason}`,
      projectId: entry.projectId,
      projectCode: entry.projectCode,
      costCenterId: entry.costCenterId,
      fiscalYear: entry.fiscalYear,
      fiscalPeriod: entry.fiscalPeriod,
      currencyCode: entry.currencyCode,
      exchangeRate: entry.exchangeRate,
      notes: `Póliza de reverso automático`,
      lines: (entry.lines || []).map((line) => ({
        accountId: line.accountId,
        accountCode: line.accountCode,
        description: `Reverso: ${line.description || ''}`,
        debitAmount: line.creditAmount, // Invertir
        creditAmount: line.debitAmount, // Invertir
        currencyAmount: line.currencyAmount,
        currencyCode: line.currencyCode,
        exchangeRate: line.exchangeRate,
        costCenterId: line.costCenterId,
        projectId: line.projectId,
        partnerId: line.partnerId,
        reference: line.reference,
      })),
    };

    const reversalEntry = await this.createEntry(ctx, reversalData);

    // Aprobar y contabilizar automáticamente
    await this.submitForApproval(ctx, reversalEntry.id);
    await this.approveEntry(ctx, reversalEntry.id);
    await this.postEntry(ctx, reversalEntry.id);

    // Marcar original como reversada
    entry.status = 'reversed';
    entry.reversalEntryId = reversalEntry.id;
    entry.notes = `${entry.notes || ''}\n[REVERSADA]: ${reason}`;
    entry.updatedBy = ctx.userId;

    return this.entryRepository.save(entry);
  }

  // ==================== REPORTES ====================

  async getTrialBalance(
    ctx: ServiceContext,
    fiscalYear: number,
    fiscalPeriod?: number
  ): Promise<any[]> {
    const queryBuilder = this.lineRepository
      .createQueryBuilder('line')
      .innerJoin('line.entry', 'entry')
      .innerJoin(ChartOfAccounts, 'account', 'account.id = line.accountId')
      .select([
        'line.accountId as "accountId"',
        'line.accountCode as "accountCode"',
        'account.name as "accountName"',
        'account.accountType as "accountType"',
        'account.nature as "nature"',
        'SUM(line.debitAmount) as "totalDebit"',
        'SUM(line.creditAmount) as "totalCredit"',
      ])
      .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('entry.status = :status', { status: 'posted' })
      .andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear });

    if (fiscalPeriod) {
      queryBuilder.andWhere('entry.fiscalPeriod <= :fiscalPeriod', { fiscalPeriod });
    }

    queryBuilder.groupBy('line.accountId, line.accountCode, account.name, account.accountType, account.nature');
    queryBuilder.orderBy('line.accountCode', 'ASC');

    const results = await queryBuilder.getRawMany();

    return results.map((row) => {
      const debit = parseFloat(row.totalDebit) || 0;
      const credit = parseFloat(row.totalCredit) || 0;
      const balance = row.nature === 'debit' ? debit - credit : credit - debit;

      return {
        accountId: row.accountId,
        accountCode: row.accountCode,
        accountName: row.accountName,
        accountType: row.accountType,
        totalDebit: debit,
        totalCredit: credit,
        balance,
        debitBalance: balance > 0 ? balance : 0,
        creditBalance: balance < 0 ? Math.abs(balance) : 0,
      };
    });
  }

  async getAccountLedger(
    ctx: ServiceContext,
    accountId: string,
    startDate: Date,
    endDate: Date
  ): Promise<any[]> {
    const lines = await this.lineRepository
      .createQueryBuilder('line')
      .innerJoinAndSelect('line.entry', 'entry')
      .where('line.accountId = :accountId', { accountId })
      .andWhere('entry.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('entry.status = :status', { status: 'posted' })
      .andWhere('entry.entryDate >= :startDate', { startDate })
      .andWhere('entry.entryDate <= :endDate', { endDate })
      .orderBy('entry.entryDate', 'ASC')
      .addOrderBy('entry.entryNumber', 'ASC')
      .getMany();

    let runningBalance = 0;
    return lines.map((line) => {
      runningBalance += line.debitAmount - line.creditAmount;
      return {
        date: line.entry?.entryDate,
        entryNumber: line.entry?.entryNumber,
        reference: line.entry?.reference,
        description: line.description || line.entry?.description,
        debitAmount: line.debitAmount,
        creditAmount: line.creditAmount,
        balance: runningBalance,
      };
    });
  }
}
