# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 - 2026 BMO Soluciones, S.A.
"""Employee processing service for building calculation variables."""

from __future__ import annotations

from datetime import date
from decimal import Decimal
from types import SimpleNamespace
from typing import Any

from coati_payroll.i18n import _
from coati_payroll.model import AcumuladoAnual, Empleado, Planilla
from ..domain.employee_calculation import EmpleadoCalculo
from ..repositories.acumulado_repository import AcumuladoRepository
from ..repositories.config_repository import ConfigRepository
from ..results.warning_collector import WarningCollectorProtocol


class EmployeeProcessingService:
    """Service for processing employee calculations and building variables."""

    def __init__(
        self,
        config_repository: ConfigRepository,
        acumulado_repository: AcumuladoRepository,
    ):
        self.config_repo = config_repository
        self.acumulado_repo = acumulado_repository

    def build_calculation_variables(
        self,
        emp_calculo: EmpleadoCalculo,
        planilla: Planilla,
        periodo_inicio: date,
        periodo_fin: date,
        fecha_calculo: date,
        configuracion_snapshot: dict[str, Any] | None = None,
        bootstrap_context: dict[str, Any] | None = None,
        warnings: WarningCollectorProtocol | None = None,
    ) -> dict[str, Any]:
        """Build the calculation variables for an employee."""
        empleado = emp_calculo.empleado
        tipo_planilla = planilla.tipo_planilla
        empresa = planilla.empresa

        config = self._resolve_config(planilla.empresa_id, configuracion_snapshot)

        # Calculate days in period
        dias_periodo = (periodo_fin - periodo_inicio).days + 1

        # Calculate seniority using configuration
        fecha_alta = empleado.fecha_alta or date.today()
        antiguedad_dias = (fecha_calculo - fecha_alta).days
        antiguedad_meses = antiguedad_dias // config.dias_mes_antiguedad
        antiguedad_anios = antiguedad_dias // config.dias_anio_antiguedad

        # Calculate remaining months in fiscal year (source of truth: planilla.mes_inicio_fiscal)
        mes_inicio_fiscal = int(planilla.mes_inicio_fiscal or (tipo_planilla.mes_inicio_fiscal if tipo_planilla else 1))
        meses_anio_financiero = int(config.meses_anio_financiero)
        if meses_anio_financiero <= 0:
            from ..validators import ValidationError

            raise ValidationError("Configuración inválida: meses_anio_financiero debe ser mayor que cero.")

        if not 1 <= mes_inicio_fiscal <= meses_anio_financiero:
            from ..validators import ValidationError

            raise ValidationError(
                "Configuración inválida: mes_inicio_fiscal fuera del rango del año financiero configurado."
            )

        meses_transcurridos = (fecha_calculo.month - mes_inicio_fiscal) % meses_anio_financiero
        meses_restantes = meses_anio_financiero - meses_transcurridos
        if meses_restantes <= 0:
            from ..validators import ValidationError

            raise ValidationError("Configuración inválida: meses_restantes calculado es menor o igual a cero.")

        # Build variables dictionary
        variables = {
            # Employee base data
            "salario_base": emp_calculo.salario_base,
            "salario_mensual": emp_calculo.salario_mensual,
            "tipo_cambio": emp_calculo.tipo_cambio,
            "salario_neto_inasistencia": emp_calculo.salario_neto_inasistencia,
            # Period data
            "fecha_calculo": fecha_calculo,
            "periodo_inicio": periodo_inicio,
            "periodo_fin": periodo_fin,
            "dias_periodo": Decimal(str(dias_periodo)),
            # Seniority
            "fecha_alta": fecha_alta,
            "antiguedad_dias": Decimal(str(antiguedad_dias)),
            "antiguedad_meses": Decimal(str(antiguedad_meses)),
            "antiguedad_anios": Decimal(str(antiguedad_anios)),
            # Fiscal calculations
            "meses_restantes": Decimal(str(meses_restantes)),
            "periodos_por_anio": Decimal(
                str(tipo_planilla.periodos_por_anio if tipo_planilla else meses_anio_financiero)
            ),
            # Accumulated values (will be populated from AcumuladoAnual)
            "salario_acumulado": Decimal("0.00"),
            "impuesto_acumulado": Decimal("0.00"),
            "ir_retenido_acumulado": Decimal("0.00"),
            "salario_acumulado_mes": Decimal("0.00"),
            # Absence tracking
            "inasistencia_dias": emp_calculo.inasistencia_dias,
            "inasistencia_horas": emp_calculo.inasistencia_horas,
            "inasistencia_descuento": emp_calculo.inasistencia_descuento,
        }

        salario_base_acumulado = Decimal(str(empleado.salario_acumulado or 0))
        impuesto_base_acumulado = Decimal(str(empleado.impuesto_acumulado or 0))

        es_periodo_inicial = self._is_initial_company_period(empresa, periodo_inicio, bootstrap_context)
        if es_periodo_inicial:
            variables["salario_acumulado"] = salario_base_acumulado
            variables["impuesto_acumulado"] = impuesto_base_acumulado
            variables["ir_retenido_acumulado"] = impuesto_base_acumulado
            if warnings and salario_base_acumulado == Decimal("0") and impuesto_base_acumulado == Decimal("0"):
                warnings.append(
                    _(
                        "Empleado %(codigo)s no tiene salario_acumulado ni "
                        "impuesto_acumulado para el período inicial; se continúa con el cálculo.",
                    )
                    % {"codigo": empleado.codigo_empleado}
                )

        # Add novelties
        for codigo, valor in emp_calculo.novedades.items():
            variables[f"novedad_{codigo}"] = valor

        # Load accumulated annual values
        acumulado = self._get_acumulado_anual(empleado, planilla, periodo_inicio)
        if acumulado:
            variables["salario_acumulado"] = Decimal(str(acumulado.salario_bruto_acumulado or 0))
            variables["impuesto_acumulado"] = Decimal(str(acumulado.impuesto_retenido_acumulado or 0))
            variables["ir_retenido_acumulado"] = Decimal(str(acumulado.impuesto_retenido_acumulado or 0))
            variables["salario_acumulado_mes"] = Decimal(str(acumulado.salario_acumulado_mes or 0))

            # Additional accumulated values for progressive tax calculations
            variables["salario_bruto_acumulado"] = Decimal(str(acumulado.salario_bruto_acumulado or 0))
            variables["salario_gravable_acumulado"] = Decimal(str(acumulado.salario_gravable_acumulado or 0))
            variables["deducciones_antes_impuesto_acumulado"] = Decimal(
                str(acumulado.deducciones_antes_impuesto_acumulado or 0)
            )
            variables["periodos_procesados"] = Decimal(str(acumulado.periodos_procesados or 0))
            # For monthly payroll, months worked should follow the fiscal calendar
            # (e.g. Feb payroll corresponds to period 2), not only processed runs.
            if tipo_planilla and (tipo_planilla.periodicidad or "").lower() in ("mensual", "monthly"):
                meses_previos = self._calculate_elapsed_fiscal_months_before_period(
                    fecha_referencia=periodo_fin,
                    fecha_alta=fecha_alta,
                    mes_inicio_fiscal=mes_inicio_fiscal,
                    dia_inicio_fiscal=tipo_planilla.dia_inicio_fiscal,
                )
                variables["meses_trabajados"] = Decimal(str(meses_previos))
            else:
                variables["meses_trabajados"] = Decimal(str(acumulado.periodos_procesados or 0))

            # Calculate net accumulated salary
            variables["salario_neto_acumulado"] = Decimal(str(acumulado.salario_bruto_acumulado or 0)) - Decimal(
                str(acumulado.deducciones_antes_impuesto_acumulado or 0)
            )

        # Include initial accumulated values from employee
        variables["salario_inicial_acumulado"] = salario_base_acumulado
        variables["impuesto_inicial_acumulado"] = impuesto_base_acumulado

        return variables

    def _calculate_elapsed_fiscal_months_before_period(
        self,
        fecha_referencia: date,
        fecha_alta: date,
        mes_inicio_fiscal: int,
        dia_inicio_fiscal: int,
    ) -> int:
        """Return elapsed fiscal months before the current payroll period.

        Example:
        - Fiscal start Jan 1
        - Payroll period in Feb
        -> returns 1, so formulas can compute period 2 with +1.
        """
        fiscal_year = fecha_referencia.year
        if fecha_referencia.month < mes_inicio_fiscal:
            fiscal_year -= 1

        fiscal_start = date(fiscal_year, mes_inicio_fiscal, dia_inicio_fiscal)
        employee_start_month = date(fecha_alta.year, fecha_alta.month, 1)
        effective_start = max(fiscal_start, employee_start_month)
        reference_month = date(fecha_referencia.year, fecha_referencia.month, 1)

        if reference_month < effective_start:
            return 0

        months = (reference_month.year - effective_start.year) * 12 + (reference_month.month - effective_start.month)
        return max(months, 0)

    def _resolve_config(self, empresa_id: str, configuracion_snapshot: dict[str, Any] | None) -> Any:
        if configuracion_snapshot:
            return SimpleNamespace(**configuracion_snapshot)

        return self.config_repo.get_for_empresa(empresa_id)

    def _get_acumulado_anual(
        self, empleado: Empleado, planilla: Planilla, periodo_inicio: date
    ) -> AcumuladoAnual | None:
        """Get accumulated annual values for employee."""
        if not planilla.tipo_planilla:
            return None

        tipo_planilla = planilla.tipo_planilla

        # Calculate fiscal period
        anio = periodo_inicio.year
        mes_inicio = int(planilla.mes_inicio_fiscal or tipo_planilla.mes_inicio_fiscal)
        dia_inicio = tipo_planilla.dia_inicio_fiscal

        if periodo_inicio.month < mes_inicio:
            anio -= 1

        periodo_fiscal_inicio = date(anio, mes_inicio, dia_inicio)

        # Look up existing accumulated record
        from sqlalchemy import select
        from coati_payroll.model import db

        acumulado = (
            db.session.execute(
                select(AcumuladoAnual).filter(
                    AcumuladoAnual.empleado_id == empleado.id,
                    AcumuladoAnual.tipo_planilla_id == tipo_planilla.id,
                    AcumuladoAnual.empresa_id == planilla.empresa_id,
                    AcumuladoAnual.periodo_fiscal_inicio == periodo_fiscal_inicio,
                )
            )
            .unique()
            .scalar_one_or_none()
        )

        return acumulado

    def _is_initial_company_period(
        self,
        empresa,
        periodo_inicio: date,
        bootstrap_context: dict[str, Any] | None = None,
    ) -> bool:
        """Return whether periodo_inicio matches the company's first payroll period."""
        if bootstrap_context is not None and "is_initial_period" in bootstrap_context:
            return bool(bootstrap_context["is_initial_period"])

        if not empresa:
            return False

        primer_mes = empresa.primer_mes_nomina
        primer_anio = empresa.primer_anio_nomina
        if primer_mes is None or primer_anio is None:
            return False

        return periodo_inicio.month == int(primer_mes) and periodo_inicio.year == int(primer_anio)
