# Copyright 2025 BMO Soluciones, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Concept calculator using Strategy pattern."""

from __future__ import annotations

from decimal import Decimal, ROUND_HALF_UP

from coati_payroll.enums import FormulaType
from coati_payroll.formula_engine import FormulaEngine, FormulaEngineError
from coati_payroll.model import db, Deduccion, ReglaCalculo
from ..domain.employee_calculation import EmpleadoCalculo


class ConceptCalculator:
    """Calculator for payroll concepts using Strategy pattern."""

    def __init__(self, config_repository, warnings: list[str]):
        self.config_repo = config_repository
        self.warnings = warnings

    def calculate(
        self,
        emp_calculo: EmpleadoCalculo,
        formula_tipo: str,
        monto_default: Decimal | None,
        porcentaje: Decimal | None,
        formula: dict | None,
        monto_override: Decimal | None,
        porcentaje_override: Decimal | None,
        codigo_concepto: str | None = None,
        base_calculo: str | None = None,
        unidad_calculo: str | None = None,
    ) -> Decimal:
        """Calculate concept amount."""
        # Use overrides if provided
        if monto_override:
            monto_calculado = Decimal(str(monto_override))
        elif porcentaje_override:
            monto_calculado = (emp_calculo.salario_base * Decimal(str(porcentaje_override)) / Decimal("100")).quantize(
                Decimal("0.01"), rounding=ROUND_HALF_UP
            )
        else:
            match formula_tipo:
                case FormulaType.FIJO:
                    monto_calculado = Decimal(str(monto_default or 0))

                case FormulaType.PORCENTAJE_SALARIO | FormulaType.PORCENTAJE:
                    if porcentaje:
                        monto_calculado = (
                            emp_calculo.salario_base * Decimal(str(porcentaje)) / Decimal("100")
                        ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
                    else:
                        monto_calculado = Decimal("0.00")

                case FormulaType.PORCENTAJE_BRUTO:
                    if porcentaje:
                        monto_calculado = (
                            emp_calculo.salario_bruto * Decimal(str(porcentaje)) / Decimal("100")
                        ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
                    else:
                        monto_calculado = Decimal("0.00")

                case FormulaType.HORAS:
                    monto_calculado = self._calculate_hours(emp_calculo, porcentaje, codigo_concepto, base_calculo)

                case FormulaType.DIAS:
                    monto_calculado = self._calculate_days(emp_calculo, porcentaje, codigo_concepto, base_calculo)

                case FormulaType.FORMULA:
                    monto_calculado = self._calculate_formula(emp_calculo, formula, codigo_concepto)

                case FormulaType.REGLA_CALCULO:
                    monto_calculado = self._calculate_regla_calculo(emp_calculo, codigo_concepto)

                case _:
                    monto_calculado = Decimal(str(monto_default or 0))

        # Ensure calculated amounts are never negative
        if monto_calculado < 0:
            self.warnings.append(
                f"Concepto '{codigo_concepto or 'desconocido'}': Configuración incorrecta resultó en "
                f"monto negativo ({monto_calculado}). Ajustando a 0.00. "
                f"Verifique la configuración del concepto (porcentaje o monto)."
            )
            return Decimal("0.00")

        return monto_calculado

    def _calculate_hours(
        self,
        emp_calculo: EmpleadoCalculo,
        porcentaje: Decimal | None,
        codigo_concepto: str | None,
        base_calculo: str | None,
    ) -> Decimal:
        """Calculate based on hours."""
        if not codigo_concepto or codigo_concepto not in emp_calculo.novedades:
            return Decimal("0.00")

        horas = emp_calculo.novedades[codigo_concepto]
        if horas <= 0:
            return Decimal("0.00")

        # Determine base for calculation
        if base_calculo == "salario_bruto":
            base = emp_calculo.salario_bruto
        else:
            base = emp_calculo.salario_mensual

        # Calculate hourly rate using configuration
        config = self.config_repo.get_for_empresa(emp_calculo.planilla.empresa_id)
        dias_base = Decimal(str(config.dias_mes_nomina))
        horas_dia = Decimal(str(config.horas_jornada_diaria))
        tasa_hora = (base / dias_base / horas_dia).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

        # Apply percentage
        if porcentaje:
            tasa_hora = (tasa_hora * Decimal(str(porcentaje)) / Decimal("100")).quantize(
                Decimal("0.01"), rounding=ROUND_HALF_UP
            )

        # Calculate total for hours
        return (tasa_hora * horas).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    def _calculate_days(
        self,
        emp_calculo: EmpleadoCalculo,
        porcentaje: Decimal | None,
        codigo_concepto: str | None,
        base_calculo: str | None,
    ) -> Decimal:
        """Calculate based on days."""
        if not codigo_concepto or codigo_concepto not in emp_calculo.novedades:
            return Decimal("0.00")

        dias = emp_calculo.novedades[codigo_concepto]
        if dias <= 0:
            return Decimal("0.00")

        # Determine base for calculation
        if base_calculo == "salario_bruto":
            base = emp_calculo.salario_bruto
        else:
            base = emp_calculo.salario_mensual

        # Calculate daily rate using configuration
        config = self.config_repo.get_for_empresa(emp_calculo.planilla.empresa_id)
        dias_base = Decimal(str(config.dias_mes_nomina))
        tasa_dia = (base / dias_base).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

        # Apply percentage
        if porcentaje:
            tasa_dia = (tasa_dia * Decimal(str(porcentaje)) / Decimal("100")).quantize(
                Decimal("0.01"), rounding=ROUND_HALF_UP
            )

        # Calculate total for days
        return (tasa_dia * dias).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    def _calculate_formula(
        self, emp_calculo: EmpleadoCalculo, formula: dict | None, codigo_concepto: str | None
    ) -> Decimal:
        """Calculate using formula engine."""
        if not formula or not isinstance(formula, dict):
            return Decimal("0.00")

        try:
            # Merge variables with formula inputs
            inputs = {**emp_calculo.variables_calculo}
            inputs["salario_bruto"] = emp_calculo.salario_bruto
            inputs["total_percepciones"] = emp_calculo.total_percepciones
            inputs["total_deducciones"] = emp_calculo.total_deducciones

            # Calculate before-tax deductions already processed in this period
            deducciones_antes_impuesto_periodo = Decimal("0.00")
            for ded in emp_calculo.deducciones:
                if ded.deduccion_id:
                    ded_obj = db.session.get(Deduccion, ded.deduccion_id)
                    if ded_obj and ded_obj.antes_impuesto:
                        deducciones_antes_impuesto_periodo += ded.monto
            inputs["deducciones_antes_impuesto_periodo"] = deducciones_antes_impuesto_periodo
            # Legacy alias for backward compatibility (deprecated but kept to avoid breaking existing schemas)
            inputs["inss_periodo"] = deducciones_antes_impuesto_periodo
            # New generic aliases (preferred for new schemas)
            inputs["pre_tax_deductions"] = deducciones_antes_impuesto_periodo
            inputs["social_security_deduction"] = deducciones_antes_impuesto_periodo

            engine = FormulaEngine(formula)
            result = engine.execute(inputs)
            return Decimal(str(result.get("output", 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        except FormulaEngineError as e:
            self.warnings.append(f"Error en fórmula: {str(e)}")
            return Decimal("0.00")

    def _calculate_regla_calculo(self, emp_calculo: EmpleadoCalculo, codigo_concepto: str | None) -> Decimal:
        """Calculate using ReglaCalculo."""
        from sqlalchemy import select

        # Find the ReglaCalculo linked to this deduction
        regla = db.session.execute(
            select(ReglaCalculo).filter_by(deduccion_id=codigo_concepto).filter(ReglaCalculo.activo.is_(True))
        ).scalar_one_or_none()

        if not regla:
            # Try finding by deduccion_id matching deduccion's id
            deduccion_obj = db.session.execute(select(Deduccion).filter_by(codigo=codigo_concepto)).scalar_one_or_none()
            if deduccion_obj:
                regla = db.session.execute(
                    select(ReglaCalculo).filter_by(deduccion_id=deduccion_obj.id).filter(ReglaCalculo.activo.is_(True))
                ).scalar_one_or_none()

        if not regla or not regla.esquema_json:
            self.warnings.append(f"ReglaCalculo no encontrada para deducción {codigo_concepto}")
            return Decimal("0.00")

        try:
            # Prepare inputs for formula engine
            inputs = {**emp_calculo.variables_calculo}
            inputs["salario_bruto"] = emp_calculo.salario_bruto
            inputs["total_percepciones"] = emp_calculo.total_percepciones
            inputs["total_deducciones"] = emp_calculo.total_deducciones

            # Calculate before-tax deductions already processed
            deducciones_antes_impuesto_periodo = Decimal("0.00")
            for ded in emp_calculo.deducciones:
                if ded.deduccion_id:
                    ded_obj = db.session.get(Deduccion, ded.deduccion_id)
                    if ded_obj and ded_obj.antes_impuesto:
                        deducciones_antes_impuesto_periodo += ded.monto
            inputs["deducciones_antes_impuesto_periodo"] = deducciones_antes_impuesto_periodo
            # Legacy alias for backward compatibility (deprecated but kept to avoid breaking existing schemas)
            inputs["inss_periodo"] = deducciones_antes_impuesto_periodo
            # New generic aliases (preferred for new schemas)
            inputs["pre_tax_deductions"] = deducciones_antes_impuesto_periodo
            inputs["social_security_deduction"] = deducciones_antes_impuesto_periodo

            engine = FormulaEngine(regla.esquema_json)
            result = engine.execute(inputs)
            return Decimal(str(result.get("output", 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        except FormulaEngineError as e:
            self.warnings.append(f"Error en ReglaCalculo {regla.codigo}: {str(e)}")
            return Decimal("0.00")
