import { Repository } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { JournalEntry, JournalEntryLine, EntryStatus } from './entities/index.js';
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ===== Interfaces =====

export interface JournalEntryLineDto {
  accountId: string;
  partnerId?: string;
  debit: number;
  credit: number;
  description?: string;
  ref?: string;
}

export interface JournalEntryLineWithRelations extends Omit<JournalEntryLine, 'entry' | 'account'> {
  accountName?: string | null;
  accountCode?: string | null;
  partnerName?: string | null;
}

export interface JournalEntryWithRelations extends Omit<JournalEntry, 'lines' | 'company' | 'journal'> {
  companyName?: string | null;
  journalName?: string | null;
  totalDebit?: number;
  totalCredit?: number;
  lines?: JournalEntryLineWithRelations[];
}

export interface CreateJournalEntryDto {
  companyId: string;
  journalId: string;
  name: string;
  ref?: string;
  date: string;
  notes?: string;
  lines: JournalEntryLineDto[];
}

export interface UpdateJournalEntryDto {
  ref?: string | null;
  date?: string;
  notes?: string | null;
  lines?: JournalEntryLineDto[];
}

export interface JournalEntryFilters {
  companyId?: string;
  journalId?: string;
  status?: EntryStatus;
  dateFrom?: string;
  dateTo?: string;
  search?: string;
  page?: number;
  limit?: number;
}

// ===== JournalEntriesService Class =====

class JournalEntriesService {
  private entryRepository: Repository<JournalEntry>;
  private lineRepository: Repository<JournalEntryLine>;

  constructor() {
    this.entryRepository = AppDataSource.getRepository(JournalEntry);
    this.lineRepository = AppDataSource.getRepository(JournalEntryLine);
  }

  /**
   * Get all journal entries with filters and pagination
   */
  async findAll(
    tenantId: string,
    filters: JournalEntryFilters = {}
  ): Promise<{ data: JournalEntryWithRelations[]; total: number }> {
    try {
      const {
        companyId,
        journalId,
        status,
        dateFrom,
        dateTo,
        search,
        page = 1,
        limit = 20
      } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.entryRepository
        .createQueryBuilder('entry')
        .leftJoin('entry.company', 'company')
        .addSelect(['company.name'])
        .leftJoin('entry.journal', 'journal')
        .addSelect(['journal.name'])
        .where('entry.tenantId = :tenantId', { tenantId });

      // Apply filters
      if (companyId) {
        queryBuilder.andWhere('entry.companyId = :companyId', { companyId });
      }

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

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

      if (dateFrom) {
        queryBuilder.andWhere('entry.date >= :dateFrom', { dateFrom });
      }

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

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

      // Get total count
      const total = await queryBuilder.getCount();

      // Get paginated results
      const entries = await queryBuilder
        .orderBy('entry.date', 'DESC')
        .addOrderBy('entry.name', 'DESC')
        .skip(skip)
        .take(limit)
        .getMany();

      // Get totals for each entry using subquery
      const entryIds = entries.map(e => e.id);
      let totalsMap: Map<string, { totalDebit: number; totalCredit: number }> = new Map();

      if (entryIds.length > 0) {
        const totals = await this.lineRepository
          .createQueryBuilder('line')
          .select('line.entryId', 'entryId')
          .addSelect('COALESCE(SUM(line.debit), 0)', 'totalDebit')
          .addSelect('COALESCE(SUM(line.credit), 0)', 'totalCredit')
          .where('line.entryId IN (:...entryIds)', { entryIds })
          .groupBy('line.entryId')
          .getRawMany();

        totals.forEach(t => {
          totalsMap.set(t.entryId, {
            totalDebit: parseFloat(t.totalDebit) || 0,
            totalCredit: parseFloat(t.totalCredit) || 0,
          });
        });
      }

      // Map to include relation names and totals
      const data: JournalEntryWithRelations[] = entries.map(entry => {
        const entryTotals = totalsMap.get(entry.id) || { totalDebit: 0, totalCredit: 0 };
        return {
          ...entry,
          companyName: entry.company?.name,
          journalName: entry.journal?.name,
          totalDebit: entryTotals.totalDebit,
          totalCredit: entryTotals.totalCredit,
        };
      });

      logger.debug('Journal entries retrieved', { tenantId, count: data.length, total });

      return { data, total };
    } catch (error) {
      logger.error('Error retrieving journal entries', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get journal entry by ID with lines
   */
  async findById(id: string, tenantId: string): Promise<JournalEntryWithRelations> {
    try {
      const entry = await this.entryRepository
        .createQueryBuilder('entry')
        .leftJoin('entry.company', 'company')
        .addSelect(['company.name'])
        .leftJoin('entry.journal', 'journal')
        .addSelect(['journal.name'])
        .where('entry.id = :id', { id })
        .andWhere('entry.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!entry) {
        throw new NotFoundError('Poliza no encontrada');
      }

      // Get lines with relations
      const lines = await this.lineRepository
        .createQueryBuilder('line')
        .leftJoin('line.account', 'account')
        .addSelect(['account.name', 'account.code'])
        .leftJoin('financial.partner', 'partner', 'line.partnerId = partner.id')
        .addSelect(['partner.name'])
        .where('line.entryId = :entryId', { entryId: id })
        .orderBy('line.createdAt', 'ASC')
        .getMany();

      // If partner join didn't work with TypeORM, use raw query for partner names
      const linesWithPartners = await this.lineRepository.query(
        `SELECT jel.*,
                a.name as account_name,
                a.code as account_code,
                p.name as partner_name
         FROM financial.journal_entry_lines jel
         LEFT JOIN financial.accounts a ON jel.account_id = a.id
         LEFT JOIN core.partners p ON jel.partner_id = p.id
         WHERE jel.entry_id = $1
         ORDER BY jel.created_at`,
        [id]
      );

      // Map lines to camelCase
      const mappedLines: JournalEntryLineWithRelations[] = linesWithPartners.map((line: any) => ({
        id: line.id,
        entryId: line.entry_id,
        tenantId: line.tenant_id,
        accountId: line.account_id,
        partnerId: line.partner_id,
        debit: parseFloat(line.debit) || 0,
        credit: parseFloat(line.credit) || 0,
        description: line.description,
        ref: line.ref,
        createdAt: line.created_at,
        accountName: line.account_name,
        accountCode: line.account_code,
        partnerName: line.partner_name,
      }));

      const totalDebit = mappedLines.reduce((sum, l) => sum + Number(l.debit), 0);
      const totalCredit = mappedLines.reduce((sum, l) => sum + Number(l.credit), 0);

      return {
        ...entry,
        companyName: entry.company?.name,
        journalName: entry.journal?.name,
        lines: mappedLines,
        totalDebit,
        totalCredit,
      };
    } catch (error) {
      logger.error('Error finding journal entry', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new journal entry with lines
   * Uses QueryRunner for transaction management
   */
  async create(
    dto: CreateJournalEntryDto,
    tenantId: string,
    userId: string
  ): Promise<JournalEntryWithRelations> {
    // Validate lines balance
    const totalDebit = dto.lines.reduce((sum, l) => sum + l.debit, 0);
    const totalCredit = dto.lines.reduce((sum, l) => sum + l.credit, 0);

    if (Math.abs(totalDebit - totalCredit) > 0.01) {
      throw new ValidationError('La poliza no esta balanceada. Debitos y creditos deben ser iguales.');
    }

    if (dto.lines.length < 2) {
      throw new ValidationError('La poliza debe tener al menos 2 lineas.');
    }

    // Use QueryRunner for transaction
    const queryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Create entry
      const entry = queryRunner.manager.create(JournalEntry, {
        tenantId,
        companyId: dto.companyId,
        journalId: dto.journalId,
        name: dto.name,
        ref: dto.ref || null,
        date: new Date(dto.date),
        notes: dto.notes || null,
        status: EntryStatus.DRAFT,
        createdBy: userId,
      });

      const savedEntry = await queryRunner.manager.save(JournalEntry, entry);

      // Create lines
      for (const lineDto of dto.lines) {
        const line = queryRunner.manager.create(JournalEntryLine, {
          entryId: savedEntry.id,
          tenantId,
          accountId: lineDto.accountId,
          partnerId: lineDto.partnerId || null,
          debit: lineDto.debit,
          credit: lineDto.credit,
          description: lineDto.description || null,
          ref: lineDto.ref || null,
        });
        await queryRunner.manager.save(JournalEntryLine, line);
      }

      await queryRunner.commitTransaction();

      logger.info('Journal entry created', {
        entryId: savedEntry.id,
        tenantId,
        name: savedEntry.name,
        createdBy: userId,
      });

      return this.findById(savedEntry.id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error creating journal entry', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Update a journal entry (only draft entries)
   * Uses QueryRunner for transaction management
   */
  async update(
    id: string,
    dto: UpdateJournalEntryDto,
    tenantId: string,
    userId: string
  ): Promise<JournalEntryWithRelations> {
    const existing = await this.findById(id, tenantId);

    if (existing.status !== EntryStatus.DRAFT) {
      throw new ConflictError('Solo se pueden modificar polizas en estado borrador');
    }

    // Use QueryRunner for transaction
    const queryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Update entry header
      const updateData: Partial<JournalEntry> = {
        updatedBy: userId,
        updatedAt: new Date(),
      };

      if (dto.ref !== undefined) updateData.ref = dto.ref;
      if (dto.date !== undefined) updateData.date = new Date(dto.date);
      if (dto.notes !== undefined) updateData.notes = dto.notes;

      await queryRunner.manager.update(JournalEntry, { id, tenantId }, updateData);

      // Update lines if provided
      if (dto.lines) {
        const totalDebit = dto.lines.reduce((sum, l) => sum + l.debit, 0);
        const totalCredit = dto.lines.reduce((sum, l) => sum + l.credit, 0);

        if (Math.abs(totalDebit - totalCredit) > 0.01) {
          throw new ValidationError('La poliza no esta balanceada');
        }

        // Delete existing lines
        await queryRunner.manager.delete(JournalEntryLine, { entryId: id });

        // Insert new lines
        for (const lineDto of dto.lines) {
          const line = queryRunner.manager.create(JournalEntryLine, {
            entryId: id,
            tenantId,
            accountId: lineDto.accountId,
            partnerId: lineDto.partnerId || null,
            debit: lineDto.debit,
            credit: lineDto.credit,
            description: lineDto.description || null,
            ref: lineDto.ref || null,
          });
          await queryRunner.manager.save(JournalEntryLine, line);
        }
      }

      await queryRunner.commitTransaction();

      logger.info('Journal entry updated', {
        entryId: id,
        tenantId,
        updatedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error updating journal entry', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Post a journal entry (draft -> posted)
   */
  async post(id: string, tenantId: string, userId: string): Promise<JournalEntryWithRelations> {
    try {
      const entry = await this.findById(id, tenantId);

      if (entry.status !== EntryStatus.DRAFT) {
        throw new ConflictError('Solo se pueden publicar polizas en estado borrador');
      }

      // Validate balance
      if (Math.abs((entry.totalDebit || 0) - (entry.totalCredit || 0)) > 0.01) {
        throw new ValidationError('La poliza no esta balanceada');
      }

      await this.entryRepository.update(
        { id, tenantId },
        {
          status: EntryStatus.POSTED,
          postedAt: new Date(),
          postedBy: userId,
          updatedAt: new Date(),
          updatedBy: userId,
        }
      );

      logger.info('Journal entry posted', {
        entryId: id,
        tenantId,
        postedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      logger.error('Error posting journal entry', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Cancel a journal entry
   */
  async cancel(id: string, tenantId: string, userId: string): Promise<JournalEntryWithRelations> {
    try {
      const entry = await this.findById(id, tenantId);

      if (entry.status === EntryStatus.CANCELLED) {
        throw new ConflictError('La poliza ya esta cancelada');
      }

      await this.entryRepository.update(
        { id, tenantId },
        {
          status: EntryStatus.CANCELLED,
          cancelledAt: new Date(),
          cancelledBy: userId,
          updatedAt: new Date(),
          updatedBy: userId,
        }
      );

      logger.info('Journal entry cancelled', {
        entryId: id,
        tenantId,
        cancelledBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      logger.error('Error cancelling journal entry', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Delete a journal entry (only draft entries, hard delete)
   */
  async delete(id: string, tenantId: string): Promise<void> {
    try {
      const entry = await this.findById(id, tenantId);

      if (entry.status !== EntryStatus.DRAFT) {
        throw new ConflictError('Solo se pueden eliminar polizas en estado borrador');
      }

      // Lines will be deleted automatically due to CASCADE on the relation
      await this.entryRepository.delete({ id, tenantId });

      logger.info('Journal entry deleted', {
        entryId: id,
        tenantId,
      });
    } catch (error) {
      logger.error('Error deleting journal entry', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }
}

// ===== Export Singleton Instance =====

export const journalEntriesService = new JournalEntriesService();

// Re-export EntryStatus for backwards compatibility
export { EntryStatus };
