import { query, queryOne, getClient } from '../../../config/database.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';
import {
  CreateBankStatementDto,
  BankStatementFilters,
  BankStatementWithLines,
  BankStatementLineResponse,
  SuggestedMatch,
} from '../dto/create-bank-statement.dto.js';
import {
  ReconcileResult,
  AutoReconcileResult,
  MatchCandidate,
  FindMatchCandidatesDto,
  CreateReconciliationRuleDto,
  UpdateReconciliationRuleDto,
} from '../dto/reconcile-line.dto.js';

/**
 * Representacion de extracto bancario
 */
export interface BankStatement {
  id: string;
  tenant_id: string;
  company_id: string | null;
  bank_account_id: string | null;
  bank_account_name?: string;
  statement_date: Date;
  opening_balance: number;
  closing_balance: number;
  status: 'draft' | 'reconciling' | 'reconciled';
  imported_at: Date | null;
  imported_by: string | null;
  reconciled_at: Date | null;
  reconciled_by: string | null;
  created_at: Date;
}

/**
 * Representacion de regla de conciliacion
 */
export interface ReconciliationRule {
  id: string;
  tenant_id: string;
  company_id: string | null;
  name: string;
  match_type: 'exact_amount' | 'reference_contains' | 'partner_name';
  match_value: string;
  auto_account_id: string | null;
  auto_account_name?: string;
  is_active: boolean;
  priority: number;
  created_at: Date;
}

/**
 * Servicio para conciliacion bancaria
 */
class BankReconciliationService {
  // ==========================================
  // EXTRACTOS BANCARIOS
  // ==========================================

  /**
   * Importar un extracto bancario con sus lineas
   */
  async importStatement(
    dto: CreateBankStatementDto,
    tenantId: string,
    userId: string
  ): Promise<BankStatement> {
    // Validaciones
    if (!dto.lines || dto.lines.length === 0) {
      throw new ValidationError('El extracto debe tener al menos una linea');
    }

    // Validar que el balance cuadre
    const calculatedClosing = dto.opening_balance + dto.lines.reduce((sum, line) => sum + line.amount, 0);
    if (Math.abs(calculatedClosing - dto.closing_balance) > 0.01) {
      throw new ValidationError(
        `El balance no cuadra. Apertura (${dto.opening_balance}) + Movimientos (${dto.lines.reduce((s, l) => s + l.amount, 0)}) = ${calculatedClosing}, pero cierre declarado es ${dto.closing_balance}`
      );
    }

    const client = await getClient();
    try {
      await client.query('BEGIN');

      // Crear el extracto
      const statementResult = await client.query<BankStatement>(
        `INSERT INTO financial.bank_statements (
          tenant_id, company_id, bank_account_id, statement_date,
          opening_balance, closing_balance, status,
          imported_at, imported_by, created_by
        )
        VALUES ($1, $2, $3, $4, $5, $6, 'draft', CURRENT_TIMESTAMP, $7, $7)
        RETURNING *`,
        [
          tenantId,
          dto.company_id || null,
          dto.bank_account_id || null,
          dto.statement_date,
          dto.opening_balance,
          dto.closing_balance,
          userId,
        ]
      );

      const statement = statementResult.rows[0];

      // Insertar las lineas
      for (const line of dto.lines) {
        await client.query(
          `INSERT INTO financial.bank_statement_lines (
            statement_id, tenant_id, transaction_date, value_date,
            description, reference, amount, partner_id, notes
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
          [
            statement.id,
            tenantId,
            line.transaction_date,
            line.value_date || null,
            line.description || null,
            line.reference || null,
            line.amount,
            line.partner_id || null,
            line.notes || null,
          ]
        );
      }

      await client.query('COMMIT');
      return statement;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * Obtener lista de extractos con filtros
   */
  async findAll(
    tenantId: string,
    filters: BankStatementFilters = {}
  ): Promise<{ data: BankStatement[]; total: number }> {
    const { company_id, bank_account_id, status, date_from, date_to, page = 1, limit = 20 } = filters;
    const offset = (page - 1) * limit;

    let whereClause = 'WHERE bs.tenant_id = $1';
    const params: unknown[] = [tenantId];
    let paramIndex = 2;

    if (company_id) {
      whereClause += ` AND bs.company_id = $${paramIndex++}`;
      params.push(company_id);
    }

    if (bank_account_id) {
      whereClause += ` AND bs.bank_account_id = $${paramIndex++}`;
      params.push(bank_account_id);
    }

    if (status) {
      whereClause += ` AND bs.status = $${paramIndex++}`;
      params.push(status);
    }

    if (date_from) {
      whereClause += ` AND bs.statement_date >= $${paramIndex++}`;
      params.push(date_from);
    }

    if (date_to) {
      whereClause += ` AND bs.statement_date <= $${paramIndex++}`;
      params.push(date_to);
    }

    const countResult = await queryOne<{ count: string }>(
      `SELECT COUNT(*) as count FROM financial.bank_statements bs ${whereClause}`,
      params
    );

    params.push(limit, offset);
    const data = await query<BankStatement>(
      `SELECT bs.*,
              a.name as bank_account_name,
              financial.get_reconciliation_progress(bs.id) as reconciliation_progress
       FROM financial.bank_statements bs
       LEFT JOIN financial.accounts a ON bs.bank_account_id = a.id
       ${whereClause}
       ORDER BY bs.statement_date DESC, bs.created_at DESC
       LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
      params
    );

    return {
      data,
      total: parseInt(countResult?.count || '0', 10),
    };
  }

  /**
   * Obtener un extracto con todas sus lineas
   */
  async getStatementWithLines(id: string, tenantId: string): Promise<BankStatementWithLines> {
    const statement = await queryOne<BankStatementWithLines>(
      `SELECT bs.*,
              a.name as bank_account_name,
              financial.get_reconciliation_progress(bs.id) as reconciliation_progress
       FROM financial.bank_statements bs
       LEFT JOIN financial.accounts a ON bs.bank_account_id = a.id
       WHERE bs.id = $1 AND bs.tenant_id = $2`,
      [id, tenantId]
    );

    if (!statement) {
      throw new NotFoundError('Extracto bancario no encontrado');
    }

    // Obtener lineas
    const lines = await query<BankStatementLineResponse>(
      `SELECT bsl.*,
              p.name as partner_name
       FROM financial.bank_statement_lines bsl
       LEFT JOIN core.partners p ON bsl.partner_id = p.id
       WHERE bsl.statement_id = $1
       ORDER BY bsl.transaction_date, bsl.created_at`,
      [id]
    );

    // Calcular balance calculado
    const calculatedBalance =
      Number(statement.opening_balance) + lines.reduce((sum, line) => sum + Number(line.amount), 0);

    statement.lines = lines;
    statement.calculated_balance = calculatedBalance;

    return statement;
  }

  /**
   * Eliminar un extracto (solo en estado draft)
   */
  async deleteStatement(id: string, tenantId: string): Promise<void> {
    const statement = await queryOne<BankStatement>(
      `SELECT * FROM financial.bank_statements WHERE id = $1 AND tenant_id = $2`,
      [id, tenantId]
    );

    if (!statement) {
      throw new NotFoundError('Extracto bancario no encontrado');
    }

    if (statement.status !== 'draft') {
      throw new ValidationError('Solo se pueden eliminar extractos en estado borrador');
    }

    await query(`DELETE FROM financial.bank_statements WHERE id = $1`, [id]);
  }

  // ==========================================
  // CONCILIACION
  // ==========================================

  /**
   * Ejecutar auto-conciliacion de un extracto
   * Busca matches automaticos por monto, fecha y referencia
   */
  async autoReconcile(statementId: string, tenantId: string, userId: string): Promise<AutoReconcileResult> {
    const statement = await this.getStatementWithLines(statementId, tenantId);

    if (statement.status === 'reconciled') {
      throw new ValidationError('El extracto ya esta completamente conciliado');
    }

    // Cambiar estado a reconciling si esta en draft
    if (statement.status === 'draft') {
      await query(
        `UPDATE financial.bank_statements SET status = 'reconciling', updated_by = $1, updated_at = CURRENT_TIMESTAMP
         WHERE id = $2`,
        [userId, statementId]
      );
    }

    const result: AutoReconcileResult = {
      total_lines: statement.lines.length,
      reconciled_count: 0,
      unreconciled_count: 0,
      reconciled_lines: [],
      lines_with_suggestions: [],
    };

    // Obtener reglas activas
    const rules = await query<ReconciliationRule>(
      `SELECT * FROM financial.bank_reconciliation_rules
       WHERE tenant_id = $1 AND is_active = true
       ORDER BY priority DESC`,
      [tenantId]
    );

    // Procesar cada linea no conciliada
    for (const line of statement.lines) {
      if (line.is_reconciled) {
        continue;
      }

      // Buscar candidatos a match
      const candidates = await this.findMatchCandidates(
        {
          amount: Math.abs(Number(line.amount)),
          date: line.transaction_date.toString(),
          reference: line.reference || undefined,
          partner_id: line.partner_id || undefined,
          amount_tolerance: 0,
          date_tolerance_days: 3,
          limit: 5,
        },
        tenantId,
        statement.bank_account_id || undefined
      );

      // Aplicar reglas personalizadas
      for (const rule of rules) {
        const ruleMatch = this.applyRule(rule, line);
        if (ruleMatch) {
          // Si la regla tiene cuenta auto, se podria crear asiento automatico
          // Por ahora solo marcamos como sugerencia
        }
      }

      // Si hay un match con confianza >= 90%, conciliar automaticamente
      const exactMatch = candidates.find((c) => c.confidence >= 90);
      if (exactMatch) {
        try {
          await this.reconcileLine(line.id, exactMatch.id, tenantId, userId);
          result.reconciled_count++;
          result.reconciled_lines.push({
            statement_line_id: line.id,
            entry_line_id: exactMatch.id,
            match_type: exactMatch.match_type,
            confidence: exactMatch.confidence,
          });
        } catch {
          // Si falla, agregar a sugerencias
          result.lines_with_suggestions.push({
            statement_line_id: line.id,
            suggestions: candidates.length,
          });
          result.unreconciled_count++;
        }
      } else if (candidates.length > 0) {
        result.lines_with_suggestions.push({
          statement_line_id: line.id,
          suggestions: candidates.length,
        });
        result.unreconciled_count++;
      } else {
        result.unreconciled_count++;
      }
    }

    return result;
  }

  /**
   * Buscar lineas de asiento candidatas a conciliar
   */
  async findMatchCandidates(
    dto: FindMatchCandidatesDto,
    tenantId: string,
    bankAccountId?: string
  ): Promise<MatchCandidate[]> {
    const { amount, date, reference, partner_id, amount_tolerance = 0, date_tolerance_days = 3, limit = 10 } = dto;

    let whereClause = `
      WHERE jel.tenant_id = $1
        AND je.status = 'posted'
        AND NOT EXISTS (
          SELECT 1 FROM financial.bank_statement_lines bsl
          WHERE bsl.reconciled_entry_id = jel.id
        )
    `;
    const params: unknown[] = [tenantId];
    let paramIndex = 2;

    // Filtrar por cuenta bancaria si se especifica
    if (bankAccountId) {
      whereClause += ` AND jel.account_id = $${paramIndex++}`;
      params.push(bankAccountId);
    }

    // Filtrar por monto con tolerancia
    const amountMin = amount * (1 - amount_tolerance);
    const amountMax = amount * (1 + amount_tolerance);
    whereClause += ` AND (
      (jel.debit BETWEEN $${paramIndex} AND $${paramIndex + 1})
      OR (jel.credit BETWEEN $${paramIndex} AND $${paramIndex + 1})
    )`;
    params.push(amountMin, amountMax);
    paramIndex += 2;

    // Filtrar por fecha con tolerancia
    if (date) {
      whereClause += ` AND je.date BETWEEN ($${paramIndex}::date - interval '${date_tolerance_days} days') AND ($${paramIndex}::date + interval '${date_tolerance_days} days')`;
      params.push(date);
      paramIndex++;
    }

    // Filtrar por partner si se especifica
    if (partner_id) {
      whereClause += ` AND jel.partner_id = $${paramIndex++}`;
      params.push(partner_id);
    }

    params.push(limit);

    const candidates = await query<MatchCandidate>(
      `SELECT
        jel.id,
        jel.entry_id,
        je.name as entry_name,
        je.ref as entry_ref,
        je.date as entry_date,
        jel.account_id,
        a.code as account_code,
        a.name as account_name,
        jel.debit,
        jel.credit,
        (jel.debit - jel.credit) as net_amount,
        jel.description,
        jel.partner_id,
        p.name as partner_name
       FROM financial.journal_entry_lines jel
       INNER JOIN financial.journal_entries je ON jel.entry_id = je.id
       INNER JOIN financial.accounts a ON jel.account_id = a.id
       LEFT JOIN core.partners p ON jel.partner_id = p.id
       ${whereClause}
       ORDER BY je.date DESC
       LIMIT $${paramIndex}`,
      params
    );

    // Calcular confianza y tipo de match para cada candidato
    return candidates.map((c) => {
      let confidence = 50; // Base
      let matchType: MatchCandidate['match_type'] = 'exact_amount';

      const candidateAmount = Math.abs(Number(c.debit) - Number(c.credit));

      // Match exacto de monto
      if (Math.abs(candidateAmount - amount) < 0.01) {
        confidence += 30;
        matchType = 'exact_amount';
      }

      // Match de fecha exacta
      if (date && c.entry_date.toString().substring(0, 10) === date.substring(0, 10)) {
        confidence += 15;
        matchType = 'amount_date';
      }

      // Match de referencia
      if (reference && c.entry_ref && c.entry_ref.toLowerCase().includes(reference.toLowerCase())) {
        confidence += 20;
        matchType = 'reference';
      }

      // Match de partner
      if (partner_id && c.partner_id === partner_id) {
        confidence += 15;
        matchType = 'partner';
      }

      return {
        ...c,
        match_type: matchType,
        confidence: Math.min(100, confidence),
      };
    });
  }

  /**
   * Conciliar manualmente una linea de extracto con una linea de asiento
   */
  async reconcileLine(
    lineId: string,
    entryLineId: string,
    tenantId: string,
    userId: string
  ): Promise<ReconcileResult> {
    // Verificar que la linea de extracto existe y no esta conciliada
    const line = await queryOne<BankStatementLineResponse>(
      `SELECT * FROM financial.bank_statement_lines WHERE id = $1 AND tenant_id = $2`,
      [lineId, tenantId]
    );

    if (!line) {
      throw new NotFoundError('Linea de extracto no encontrada');
    }

    if (line.is_reconciled) {
      throw new ValidationError('La linea ya esta conciliada');
    }

    // Verificar que la linea de asiento existe y no esta conciliada con otra linea
    const entryLine = await queryOne<{ id: string; debit: number; credit: number }>(
      `SELECT jel.* FROM financial.journal_entry_lines jel
       INNER JOIN financial.journal_entries je ON jel.entry_id = je.id
       WHERE jel.id = $1 AND jel.tenant_id = $2 AND je.status = 'posted'`,
      [entryLineId, tenantId]
    );

    if (!entryLine) {
      throw new NotFoundError('Linea de asiento no encontrada o no publicada');
    }

    // Verificar que no este ya conciliada
    const alreadyReconciled = await queryOne<{ id: string }>(
      `SELECT id FROM financial.bank_statement_lines WHERE reconciled_entry_id = $1`,
      [entryLineId]
    );

    if (alreadyReconciled) {
      throw new ValidationError('La linea de asiento ya esta conciliada con otra linea de extracto');
    }

    // Actualizar la linea de extracto
    await query(
      `UPDATE financial.bank_statement_lines SET
        is_reconciled = true,
        reconciled_entry_id = $1,
        reconciled_at = CURRENT_TIMESTAMP,
        reconciled_by = $2
       WHERE id = $3`,
      [entryLineId, userId, lineId]
    );

    return {
      success: true,
      statement_line_id: lineId,
      entry_line_id: entryLineId,
    };
  }

  /**
   * Deshacer la conciliacion de una linea
   */
  async unreconcileLine(lineId: string, tenantId: string): Promise<void> {
    const line = await queryOne<BankStatementLineResponse>(
      `SELECT * FROM financial.bank_statement_lines WHERE id = $1 AND tenant_id = $2`,
      [lineId, tenantId]
    );

    if (!line) {
      throw new NotFoundError('Linea de extracto no encontrada');
    }

    if (!line.is_reconciled) {
      throw new ValidationError('La linea no esta conciliada');
    }

    await query(
      `UPDATE financial.bank_statement_lines SET
        is_reconciled = false,
        reconciled_entry_id = NULL,
        reconciled_at = NULL,
        reconciled_by = NULL
       WHERE id = $1`,
      [lineId]
    );
  }

  /**
   * Cerrar un extracto completamente conciliado
   */
  async closeStatement(statementId: string, tenantId: string, userId: string): Promise<BankStatement> {
    const statement = await this.getStatementWithLines(statementId, tenantId);

    if (statement.status === 'reconciled') {
      throw new ValidationError('El extracto ya esta cerrado');
    }

    // Verificar que todas las lineas esten conciliadas
    const unreconciledLines = statement.lines.filter((l) => !l.is_reconciled);
    if (unreconciledLines.length > 0) {
      throw new ValidationError(
        `No se puede cerrar el extracto. Hay ${unreconciledLines.length} linea(s) sin conciliar`
      );
    }

    await query(
      `UPDATE financial.bank_statements SET
        status = 'reconciled',
        reconciled_at = CURRENT_TIMESTAMP,
        reconciled_by = $1,
        updated_by = $1,
        updated_at = CURRENT_TIMESTAMP
       WHERE id = $2`,
      [userId, statementId]
    );

    return this.findById(statementId, tenantId);
  }

  /**
   * Obtener un extracto por ID
   */
  async findById(id: string, tenantId: string): Promise<BankStatement> {
    const statement = await queryOne<BankStatement>(
      `SELECT bs.*, a.name as bank_account_name
       FROM financial.bank_statements bs
       LEFT JOIN financial.accounts a ON bs.bank_account_id = a.id
       WHERE bs.id = $1 AND bs.tenant_id = $2`,
      [id, tenantId]
    );

    if (!statement) {
      throw new NotFoundError('Extracto bancario no encontrado');
    }

    return statement;
  }

  // ==========================================
  // REGLAS DE CONCILIACION
  // ==========================================

  /**
   * Crear una regla de conciliacion
   */
  async createRule(dto: CreateReconciliationRuleDto, tenantId: string, userId: string): Promise<ReconciliationRule> {
    const result = await queryOne<ReconciliationRule>(
      `INSERT INTO financial.bank_reconciliation_rules (
        tenant_id, company_id, name, match_type, match_value,
        auto_account_id, priority, is_active, created_by
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
      RETURNING *`,
      [
        tenantId,
        dto.company_id || null,
        dto.name,
        dto.match_type,
        dto.match_value,
        dto.auto_account_id || null,
        dto.priority || 0,
        dto.is_active !== false,
        userId,
      ]
    );

    return result!;
  }

  /**
   * Obtener reglas de conciliacion
   */
  async findRules(tenantId: string, companyId?: string): Promise<ReconciliationRule[]> {
    let whereClause = 'WHERE r.tenant_id = $1';
    const params: unknown[] = [tenantId];

    if (companyId) {
      whereClause += ' AND (r.company_id = $2 OR r.company_id IS NULL)';
      params.push(companyId);
    }

    return query<ReconciliationRule>(
      `SELECT r.*, a.name as auto_account_name
       FROM financial.bank_reconciliation_rules r
       LEFT JOIN financial.accounts a ON r.auto_account_id = a.id
       ${whereClause}
       ORDER BY r.priority DESC, r.name`,
      params
    );
  }

  /**
   * Actualizar una regla de conciliacion
   */
  async updateRule(
    id: string,
    dto: UpdateReconciliationRuleDto,
    tenantId: string,
    userId: string
  ): Promise<ReconciliationRule> {
    const existing = await queryOne<ReconciliationRule>(
      `SELECT * FROM financial.bank_reconciliation_rules WHERE id = $1 AND tenant_id = $2`,
      [id, tenantId]
    );

    if (!existing) {
      throw new NotFoundError('Regla de conciliacion no encontrada');
    }

    const updateFields: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    if (dto.name !== undefined) {
      updateFields.push(`name = $${paramIndex++}`);
      values.push(dto.name);
    }
    if (dto.match_type !== undefined) {
      updateFields.push(`match_type = $${paramIndex++}`);
      values.push(dto.match_type);
    }
    if (dto.match_value !== undefined) {
      updateFields.push(`match_value = $${paramIndex++}`);
      values.push(dto.match_value);
    }
    if (dto.auto_account_id !== undefined) {
      updateFields.push(`auto_account_id = $${paramIndex++}`);
      values.push(dto.auto_account_id);
    }
    if (dto.priority !== undefined) {
      updateFields.push(`priority = $${paramIndex++}`);
      values.push(dto.priority);
    }
    if (dto.is_active !== undefined) {
      updateFields.push(`is_active = $${paramIndex++}`);
      values.push(dto.is_active);
    }

    if (updateFields.length === 0) {
      return existing;
    }

    updateFields.push(`updated_by = $${paramIndex++}`);
    values.push(userId);
    updateFields.push(`updated_at = CURRENT_TIMESTAMP`);

    values.push(id, tenantId);

    await query(
      `UPDATE financial.bank_reconciliation_rules SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex++} AND tenant_id = $${paramIndex}`,
      values
    );

    return this.findRuleById(id, tenantId);
  }

  /**
   * Obtener regla por ID
   */
  async findRuleById(id: string, tenantId: string): Promise<ReconciliationRule> {
    const rule = await queryOne<ReconciliationRule>(
      `SELECT r.*, a.name as auto_account_name
       FROM financial.bank_reconciliation_rules r
       LEFT JOIN financial.accounts a ON r.auto_account_id = a.id
       WHERE r.id = $1 AND r.tenant_id = $2`,
      [id, tenantId]
    );

    if (!rule) {
      throw new NotFoundError('Regla de conciliacion no encontrada');
    }

    return rule;
  }

  /**
   * Eliminar una regla
   */
  async deleteRule(id: string, tenantId: string): Promise<void> {
    const existing = await queryOne<ReconciliationRule>(
      `SELECT * FROM financial.bank_reconciliation_rules WHERE id = $1 AND tenant_id = $2`,
      [id, tenantId]
    );

    if (!existing) {
      throw new NotFoundError('Regla de conciliacion no encontrada');
    }

    await query(`DELETE FROM financial.bank_reconciliation_rules WHERE id = $1`, [id]);
  }

  // ==========================================
  // HELPERS
  // ==========================================

  /**
   * Aplicar una regla a una linea de extracto
   */
  private applyRule(
    rule: ReconciliationRule,
    line: BankStatementLineResponse
  ): boolean {
    switch (rule.match_type) {
      case 'exact_amount':
        return Math.abs(Number(line.amount)) === parseFloat(rule.match_value);

      case 'reference_contains':
        return line.reference?.toLowerCase().includes(rule.match_value.toLowerCase()) || false;

      case 'partner_name':
        // Esto requeriria el nombre del partner, que ya esta en partner_name
        return line.partner_name?.toLowerCase().includes(rule.match_value.toLowerCase()) || false;

      default:
        return false;
    }
  }
}

export const bankReconciliationService = new BankReconciliationService();
