import { Repository, IsNull } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { FiscalYear, FiscalPeriod, FiscalPeriodStatus } from './entities/index.js';
import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ============================================================================
// INTERFACES
// ============================================================================

export interface FiscalYearWithRelations extends FiscalYear {
  companyName?: string;
}

export interface FiscalPeriodWithRelations extends FiscalPeriod {
  fiscalYearName?: string;
  closedByName?: string;
}

export interface CreateFiscalYearDto {
  companyId: string;
  name: string;
  code: string;
  dateFrom: string;
  dateTo: string;
}

export interface CreateFiscalPeriodDto {
  fiscalYearId: string;
  code: string;
  name: string;
  dateFrom: string;
  dateTo: string;
}

export interface FiscalPeriodFilters {
  companyId?: string;
  fiscalYearId?: string;
  status?: FiscalPeriodStatus;
  dateFrom?: string;
  dateTo?: string;
}

export interface PeriodStats {
  totalEntries: number;
  draftEntries: number;
  postedEntries: number;
  totalDebit: number;
  totalCredit: number;
}

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

class FiscalPeriodsService {
  private fiscalYearRepository: Repository<FiscalYear>;
  private fiscalPeriodRepository: Repository<FiscalPeriod>;

  constructor() {
    this.fiscalYearRepository = AppDataSource.getRepository(FiscalYear);
    this.fiscalPeriodRepository = AppDataSource.getRepository(FiscalPeriod);
  }

  // ==================== FISCAL YEARS ====================

  /**
   * Get all fiscal years with optional company filter
   */
  async findAllYears(
    tenantId: string,
    companyId?: string
  ): Promise<FiscalYearWithRelations[]> {
    try {
      const queryBuilder = this.fiscalYearRepository
        .createQueryBuilder('fiscalYear')
        .leftJoin('fiscalYear.company', 'company')
        .addSelect(['company.name'])
        .where('fiscalYear.tenantId = :tenantId', { tenantId });

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

      const years = await queryBuilder
        .orderBy('fiscalYear.dateFrom', 'DESC')
        .getMany();

      const data: FiscalYearWithRelations[] = years.map((year) => ({
        ...year,
        companyName: year.company?.name,
      }));

      logger.debug('Fiscal years retrieved', {
        tenantId,
        companyId,
        count: data.length,
      });

      return data;
    } catch (error) {
      logger.error('Error retrieving fiscal years', {
        error: (error as Error).message,
        tenantId,
        companyId,
      });
      throw error;
    }
  }

  /**
   * Get fiscal year by ID
   */
  async findYearById(id: string, tenantId: string): Promise<FiscalYear> {
    try {
      const year = await this.fiscalYearRepository.findOne({
        where: { id, tenantId },
      });

      if (!year) {
        throw new NotFoundError('Año fiscal no encontrado');
      }

      return year;
    } catch (error) {
      logger.error('Error finding fiscal year', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new fiscal year
   */
  async createYear(
    dto: CreateFiscalYearDto,
    tenantId: string,
    userId: string
  ): Promise<FiscalYear> {
    try {
      // Check for overlapping years using QueryBuilder
      const overlapping = await this.fiscalYearRepository
        .createQueryBuilder('fiscalYear')
        .where('fiscalYear.tenantId = :tenantId', { tenantId })
        .andWhere('fiscalYear.companyId = :companyId', { companyId: dto.companyId })
        .andWhere(
          `(fiscalYear.dateFrom, fiscalYear.dateTo) OVERLAPS (:dateFrom::date, :dateTo::date)`,
          { dateFrom: dto.dateFrom, dateTo: dto.dateTo }
        )
        .getOne();

      if (overlapping) {
        throw new ConflictError(
          'Ya existe un año fiscal que se superpone con estas fechas'
        );
      }

      // Create fiscal year
      const year = this.fiscalYearRepository.create({
        tenantId,
        companyId: dto.companyId,
        name: dto.name,
        code: dto.code,
        dateFrom: new Date(dto.dateFrom),
        dateTo: new Date(dto.dateTo),
        status: FiscalPeriodStatus.OPEN,
        createdBy: userId,
      });

      await this.fiscalYearRepository.save(year);

      logger.info('Fiscal year created', {
        yearId: year.id,
        tenantId,
        name: dto.name,
        createdBy: userId,
      });

      return year;
    } catch (error) {
      logger.error('Error creating fiscal year', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    }
  }

  // ==================== FISCAL PERIODS ====================

  /**
   * Get all fiscal periods with filters
   */
  async findAllPeriods(
    tenantId: string,
    filters: FiscalPeriodFilters = {}
  ): Promise<FiscalPeriodWithRelations[]> {
    try {
      const queryBuilder = this.fiscalPeriodRepository
        .createQueryBuilder('fiscalPeriod')
        .leftJoin('fiscalPeriod.fiscalYear', 'fiscalYear')
        .addSelect(['fiscalYear.name', 'fiscalYear.companyId'])
        .where('fiscalPeriod.tenantId = :tenantId', { tenantId });

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

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

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

      if (filters.dateFrom) {
        queryBuilder.andWhere('fiscalPeriod.dateFrom >= :dateFrom', {
          dateFrom: filters.dateFrom,
        });
      }

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

      const periods = await queryBuilder
        .orderBy('fiscalPeriod.dateFrom', 'DESC')
        .getMany();

      // Get closed_by user names using raw query
      const periodIds = periods.map((p) => p.id);
      let usersMap: Map<string, string> = new Map();

      if (periodIds.length > 0) {
        const userResults = await this.fiscalPeriodRepository.query(
          `SELECT fp.id, u.full_name
           FROM financial.fiscal_periods fp
           LEFT JOIN auth.users u ON fp.closed_by = u.id
           WHERE fp.id = ANY($1) AND fp.closed_by IS NOT NULL`,
          [periodIds]
        );

        userResults.forEach((row: any) => {
          usersMap.set(row.id, row.full_name);
        });
      }

      // Map to include relation names
      const data: FiscalPeriodWithRelations[] = periods.map((period) => ({
        ...period,
        fiscalYearName: period.fiscalYear?.name,
        closedByName: period.closedBy ? usersMap.get(period.id) : undefined,
      }));

      logger.debug('Fiscal periods retrieved', {
        tenantId,
        count: data.length,
        filters,
      });

      return data;
    } catch (error) {
      logger.error('Error retrieving fiscal periods', {
        error: (error as Error).message,
        tenantId,
        filters,
      });
      throw error;
    }
  }

  /**
   * Get fiscal period by ID with relations
   */
  async findPeriodById(
    id: string,
    tenantId: string
  ): Promise<FiscalPeriodWithRelations> {
    try {
      const period = await this.fiscalPeriodRepository
        .createQueryBuilder('fiscalPeriod')
        .leftJoin('fiscalPeriod.fiscalYear', 'fiscalYear')
        .addSelect(['fiscalYear.name'])
        .where('fiscalPeriod.id = :id', { id })
        .andWhere('fiscalPeriod.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!period) {
        throw new NotFoundError('Período fiscal no encontrado');
      }

      // Get closed_by user name if exists
      let closedByName: string | undefined = undefined;
      if (period.closedBy) {
        const userResult = await this.fiscalPeriodRepository.query(
          `SELECT full_name FROM auth.users WHERE id = $1`,
          [period.closedBy]
        );
        closedByName = userResult[0]?.full_name;
      }

      return {
        ...period,
        fiscalYearName: period.fiscalYear?.name,
        closedByName,
      };
    } catch (error) {
      logger.error('Error finding fiscal period', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Find fiscal period by date
   */
  async findPeriodByDate(
    date: Date,
    companyId: string,
    tenantId: string
  ): Promise<FiscalPeriod | null> {
    try {
      const period = await this.fiscalPeriodRepository
        .createQueryBuilder('fiscalPeriod')
        .leftJoin('fiscalPeriod.fiscalYear', 'fiscalYear')
        .where('fiscalPeriod.tenantId = :tenantId', { tenantId })
        .andWhere('fiscalYear.companyId = :companyId', { companyId })
        .andWhere(':date::date BETWEEN fiscalPeriod.dateFrom AND fiscalPeriod.dateTo', {
          date,
        })
        .getOne();

      return period;
    } catch (error) {
      logger.error('Error finding period by date', {
        error: (error as Error).message,
        date,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new fiscal period
   */
  async createPeriod(
    dto: CreateFiscalPeriodDto,
    tenantId: string,
    userId: string
  ): Promise<FiscalPeriod> {
    try {
      // Verify fiscal year exists
      await this.findYearById(dto.fiscalYearId, tenantId);

      // Check for overlapping periods in the same year
      const overlapping = await this.fiscalPeriodRepository
        .createQueryBuilder('fiscalPeriod')
        .where('fiscalPeriod.tenantId = :tenantId', { tenantId })
        .andWhere('fiscalPeriod.fiscalYearId = :fiscalYearId', {
          fiscalYearId: dto.fiscalYearId,
        })
        .andWhere(
          `(fiscalPeriod.dateFrom, fiscalPeriod.dateTo) OVERLAPS (:dateFrom::date, :dateTo::date)`,
          { dateFrom: dto.dateFrom, dateTo: dto.dateTo }
        )
        .getOne();

      if (overlapping) {
        throw new ConflictError(
          'Ya existe un período que se superpone con estas fechas'
        );
      }

      // Create fiscal period
      const period = this.fiscalPeriodRepository.create({
        tenantId,
        fiscalYearId: dto.fiscalYearId,
        code: dto.code,
        name: dto.name,
        dateFrom: new Date(dto.dateFrom),
        dateTo: new Date(dto.dateTo),
        status: FiscalPeriodStatus.OPEN,
        closedAt: null,
        closedBy: null,
        createdBy: userId,
      });

      await this.fiscalPeriodRepository.save(period);

      logger.info('Fiscal period created', {
        periodId: period.id,
        tenantId,
        name: dto.name,
        createdBy: userId,
      });

      return period;
    } catch (error) {
      logger.error('Error creating fiscal period', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    }
  }

  // ==================== PERIOD OPERATIONS ====================

  /**
   * Close a fiscal period
   * Uses database function for validation
   */
  async closePeriod(
    periodId: string,
    tenantId: string,
    userId: string
  ): Promise<FiscalPeriod> {
    try {
      // Verify period exists and belongs to tenant
      await this.findPeriodById(periodId, tenantId);

      // Use database function for atomic close with validations
      const result = await this.fiscalPeriodRepository.query(
        `SELECT * FROM financial.close_fiscal_period($1, $2)`,
        [periodId, userId]
      );

      if (!result || result.length === 0) {
        throw new Error('Error al cerrar período');
      }

      // Map snake_case result to camelCase
      const period: FiscalPeriod = {
        id: result[0].id,
        tenantId: result[0].tenant_id,
        fiscalYearId: result[0].fiscal_year_id,
        code: result[0].code,
        name: result[0].name,
        dateFrom: result[0].date_from,
        dateTo: result[0].date_to,
        status: result[0].status,
        closedAt: result[0].closed_at,
        closedBy: result[0].closed_by,
        createdAt: result[0].created_at,
        createdBy: result[0].created_by,
        fiscalYear: undefined as any,
      };

      logger.info('Fiscal period closed', { periodId, userId, tenantId });

      return period;
    } catch (error) {
      logger.error('Error closing fiscal period', {
        error: (error as Error).message,
        periodId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Reopen a fiscal period (admin only)
   */
  async reopenPeriod(
    periodId: string,
    tenantId: string,
    userId: string,
    reason?: string
  ): Promise<FiscalPeriod> {
    try {
      // Verify period exists and belongs to tenant
      await this.findPeriodById(periodId, tenantId);

      // Use database function for atomic reopen with audit
      const result = await this.fiscalPeriodRepository.query(
        `SELECT * FROM financial.reopen_fiscal_period($1, $2, $3)`,
        [periodId, userId, reason || null]
      );

      if (!result || result.length === 0) {
        throw new Error('Error al reabrir período');
      }

      // Map snake_case result to camelCase
      const period: FiscalPeriod = {
        id: result[0].id,
        tenantId: result[0].tenant_id,
        fiscalYearId: result[0].fiscal_year_id,
        code: result[0].code,
        name: result[0].name,
        dateFrom: result[0].date_from,
        dateTo: result[0].date_to,
        status: result[0].status,
        closedAt: result[0].closed_at,
        closedBy: result[0].closed_by,
        createdAt: result[0].created_at,
        createdBy: result[0].created_by,
        fiscalYear: undefined as any,
      };

      logger.warn('Fiscal period reopened', {
        periodId,
        userId,
        tenantId,
        reason,
      });

      return period;
    } catch (error) {
      logger.error('Error reopening fiscal period', {
        error: (error as Error).message,
        periodId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get statistics for a period
   */
  async getPeriodStats(periodId: string, tenantId: string): Promise<PeriodStats> {
    try {
      const stats = await this.fiscalPeriodRepository.query(
        `SELECT
           COUNT(*) as total_entries,
           COUNT(*) FILTER (WHERE status = 'draft') as draft_entries,
           COUNT(*) FILTER (WHERE status = 'posted') as posted_entries,
           COALESCE(SUM(total_debit), 0) as total_debit,
           COALESCE(SUM(total_credit), 0) as total_credit
         FROM financial.journal_entries
         WHERE fiscal_period_id = $1 AND tenant_id = $2`,
        [periodId, tenantId]
      );

      const result: PeriodStats = {
        totalEntries: parseInt(stats[0]?.total_entries || '0', 10),
        draftEntries: parseInt(stats[0]?.draft_entries || '0', 10),
        postedEntries: parseInt(stats[0]?.posted_entries || '0', 10),
        totalDebit: parseFloat(stats[0]?.total_debit || '0'),
        totalCredit: parseFloat(stats[0]?.total_credit || '0'),
      };

      logger.debug('Period stats retrieved', { periodId, tenantId, result });

      return result;
    } catch (error) {
      logger.error('Error retrieving period stats', {
        error: (error as Error).message,
        periodId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Generate monthly periods for a fiscal year
   */
  async generateMonthlyPeriods(
    fiscalYearId: string,
    tenantId: string,
    userId: string
  ): Promise<FiscalPeriod[]> {
    try {
      const year = await this.findYearById(fiscalYearId, tenantId);

      const startDate = new Date(year.dateFrom);
      const endDate = new Date(year.dateTo);
      const periods: FiscalPeriod[] = [];

      let currentDate = new Date(startDate);
      let periodNum = 1;

      while (currentDate <= endDate) {
        const periodStart = new Date(currentDate);
        const periodEnd = new Date(
          currentDate.getFullYear(),
          currentDate.getMonth() + 1,
          0
        );

        // Don't exceed the fiscal year end
        if (periodEnd > endDate) {
          periodEnd.setTime(endDate.getTime());
        }

        const monthNames = [
          'Enero',
          'Febrero',
          'Marzo',
          'Abril',
          'Mayo',
          'Junio',
          'Julio',
          'Agosto',
          'Septiembre',
          'Octubre',
          'Noviembre',
          'Diciembre',
        ];

        try {
          const period = await this.createPeriod(
            {
              fiscalYearId: fiscalYearId,
              code: String(periodNum).padStart(2, '0'),
              name: `${monthNames[periodStart.getMonth()]} ${periodStart.getFullYear()}`,
              dateFrom: periodStart.toISOString().split('T')[0],
              dateTo: periodEnd.toISOString().split('T')[0],
            },
            tenantId,
            userId
          );

          periods.push(period);
        } catch (error) {
          // Skip if period already exists (overlapping check will fail)
          logger.debug('Period creation skipped', { periodNum, error });
        }

        // Move to next month
        currentDate.setMonth(currentDate.getMonth() + 1);
        currentDate.setDate(1);
        periodNum++;
      }

      logger.info('Generated monthly periods', {
        fiscalYearId,
        tenantId,
        count: periods.length,
      });

      return periods;
    } catch (error) {
      logger.error('Error generating monthly periods', {
        error: (error as Error).message,
        fiscalYearId,
        tenantId,
      });
      throw error;
    }
  }
}

// ============================================================================
// EXPORT
// ============================================================================

export const fiscalPeriodsService = new FiscalPeriodsService();

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