from __future__ import annotations

from decimal import Decimal
from typing import Sequence

from .enums import AmountMode, BaseEffect, CalculationPhase, InputMode, PayslipSide
from .money import money
from .models import (
    PayrollBatchRequest,
    PayrollBatchResult,
    PayrollLineItem,
    PayrollRequest,
    PayrollResult,
    ResultLineItem,
)
from .policies import UgandaPayrollPolicy


class PayrollCalculator:
    def __init__(self, policy: UgandaPayrollPolicy | None = None) -> None:
        self.policy = policy or UgandaPayrollPolicy.uganda_2026()

    def calculate(self, request: PayrollRequest) -> PayrollResult:
        basic_pay = self._resolve_basic_pay(request)
        return self._calculate_from_basic(request, basic_pay)

    def calculate_batch(self, batch: PayrollBatchRequest) -> PayrollBatchResult:
        results = tuple(self.calculate(req) for req in batch.employee_requests)
        return PayrollBatchResult(
            employer=batch.employer,
            period=batch.period,
            results=results,
            total_basic_pay=money(sum(r.basic_pay for r in results)),
            total_gross_earnings=money(sum(r.gross_earnings for r in results)),
            total_nssf_employee=money(sum(r.nssf_employee for r in results)),
            total_nssf_employer=money(sum(r.nssf_employer for r in results)),
            total_paye=money(sum(r.total_paye for r in results)),
            total_lst=money(sum(r.lst for r in results)),
            total_net_pay=money(sum(r.net_pay for r in results)),
            total_employer_cost=money(sum(r.total_employer_cost for r in results)),
        )

    def _resolve_basic_pay(self, request: PayrollRequest) -> Decimal:
        target = request.normalized_target_amount
        if request.input_mode == InputMode.BASIC_PAY:
            return target
        if request.input_mode == InputMode.GROSS_PAY:
            gross_item_total = money(
                sum(
                    item.normalized_amount
                    for item in request.items
                    if item.amount_mode == AmountMode.FIXED
                    and item.category.phase == CalculationPhase.PRE_TAX
                    and item.category.cash_effect == BaseEffect.ADD
                )
            )
            basic_pay = money(target - gross_item_total)
            if basic_pay < 0:
                raise ValueError("Gross target is lower than the configured gross additions.")
            return basic_pay
        return self._solve_basic_from_net_target(request)

    def _solve_basic_from_net_target(self, request: PayrollRequest) -> Decimal:
        target_net = request.normalized_target_amount
        low = Decimal("0")
        high = max(target_net, Decimal("100000"))

        while self._calculate_from_basic(request, high).net_pay < target_net:
            high *= Decimal("2")
            if high > Decimal("1000000000"):
                raise ValueError("Could not bracket a basic pay that satisfies the requested net pay.")

        best = high
        for _ in range(80):
            mid = money((low + high) / Decimal("2"))
            result = self._calculate_from_basic(request, mid)
            if result.net_pay >= target_net:
                best = mid
                high = mid
            else:
                low = mid + Decimal("1")
        return money(best)

    def _calculate_from_basic(self, request: PayrollRequest, basic_pay: Decimal) -> PayrollResult:
        basic_pay = money(basic_pay)
        resolved_items = self._resolve_amounts(request.items, basic_pay)
        result_items: list[ResultLineItem] = []

        pre_tax_additions = Decimal("0")
        pre_tax_cash_deductions = Decimal("0")
        post_tax_additions = Decimal("0")
        post_tax_cash_deductions = Decimal("0")
        paye_base = basic_pay
        nssf_employee_base = basic_pay
        nssf_employer_base = basic_pay
        employer_cost_extras = Decimal("0")

        for item, amount in resolved_items:
            cash_impact = _apply_effect(item.category.cash_effect, amount)
            paye_base_impact = _apply_effect(item.category.paye_base_effect, amount)
            nssf_employee_impact = _apply_effect(item.category.nssf_employee_base_effect, amount)
            nssf_employer_impact = _apply_effect(item.category.nssf_employer_base_effect, amount)
            employer_cost_impact = _apply_effect(item.category.employer_cost_effect, amount)

            if item.category.phase == CalculationPhase.PRE_TAX:
                if cash_impact > 0:
                    pre_tax_additions += cash_impact
                elif cash_impact < 0:
                    pre_tax_cash_deductions += abs(cash_impact)
            else:
                if cash_impact > 0:
                    post_tax_additions += cash_impact
                elif cash_impact < 0:
                    post_tax_cash_deductions += abs(cash_impact)

            paye_base += paye_base_impact
            nssf_employee_base += nssf_employee_impact
            nssf_employer_base += nssf_employer_impact
            employer_cost_extras += employer_cost_impact

            result_items.append(
                ResultLineItem(
                    category=item.category,
                    amount=amount,
                    cash_impact=money(cash_impact),
                    paye_base_impact=money(paye_base_impact),
                    nssf_employee_base_impact=money(nssf_employee_impact),
                    nssf_employer_base_impact=money(nssf_employer_impact),
                    employer_cost_impact=money(employer_cost_impact),
                )
            )

        paye_base = money(max(paye_base, Decimal("0")))
        nssf_employee_base = money(max(nssf_employee_base, Decimal("0")))
        nssf_employer_base = money(max(nssf_employer_base, Decimal("0")))

        nssf_employee = money(nssf_employee_base * self.policy.nssf_employee_rate)
        nssf_employer = money(nssf_employer_base * self.policy.nssf_employer_rate)

        lst_basis = money(max(paye_base - nssf_employee, Decimal("0")))
        lst = self.policy.lst_policy.compute(month=request.period.month, monthly_income_after_nssf=lst_basis)

        chargeable_income = paye_base - lst if self.policy.deduct_lst_before_paye else paye_base
        chargeable_income = money(max(chargeable_income, Decimal("0")))
        total_paye = self.policy.compute_paye(request.employee.employment_tax_mode, chargeable_income)

        gross_earnings = money(basic_pay + pre_tax_additions)
        gross_cash_before_statutory = money(gross_earnings - pre_tax_cash_deductions)

        total_deductions = money(pre_tax_cash_deductions + nssf_employee + lst + total_paye + post_tax_cash_deductions)
        net_pay = money(gross_cash_before_statutory - nssf_employee - lst - total_paye + post_tax_additions - post_tax_cash_deductions)
        total_employer_cost = money(gross_earnings + nssf_employer + employer_cost_extras)

        return PayrollResult(
            employer=request.employer,
            employee=request.employee,
            period=request.period,
            basic_pay=basic_pay,
            gross_earnings=gross_earnings,
            gross_cash_before_statutory=gross_cash_before_statutory,
            chargeable_income=chargeable_income,
            paye_base=paye_base,
            total_paye=total_paye,
            lst=lst,
            nssf_employee=nssf_employee,
            nssf_employer=nssf_employer,
            total_deductions=total_deductions,
            net_pay=net_pay,
            total_employer_cost=total_employer_cost,
            items=tuple(result_items),
        )
    def _resolve_amounts(
        self, items: tuple[PayrollLineItem, ...], basic_pay: Decimal
    ) -> list[tuple[PayrollLineItem, Decimal]]:
        """Resolve percentage-based amounts to fixed decimals.

        For PERCENTAGE_OF_GROSS, we first compute gross_pay (basic + fixed pre-tax additions),
        then resolve percentage-of-gross items against that.
        """
        # First pass: compute gross from basic + fixed pre-tax additions
        fixed_pre_tax_additions = Decimal("0")
        for item in items:
            if (
                item.amount_mode == AmountMode.FIXED
                and item.category.phase == CalculationPhase.PRE_TAX
                and item.category.cash_effect == BaseEffect.ADD
            ):
                fixed_pre_tax_additions += item.normalized_amount
        gross_pay = money(basic_pay + fixed_pre_tax_additions)

        resolved: list[tuple[PayrollLineItem, Decimal]] = []
        for item in items:
            if item.amount_mode == AmountMode.FIXED:
                resolved.append((item, item.normalized_amount))
            elif item.amount_mode == AmountMode.PERCENTAGE_OF_BASIC:
                resolved.append((item, money(item.normalized_amount * basic_pay / Decimal("100"))))
            elif item.amount_mode == AmountMode.PERCENTAGE_OF_GROSS:
                resolved.append((item, money(item.normalized_amount * gross_pay / Decimal("100"))))
        return resolved


def _apply_effect(effect: BaseEffect, amount: Decimal) -> Decimal:
    resolved_amount = money(amount)
    if effect == BaseEffect.ADD:
        return resolved_amount
    if effect == BaseEffect.SUBTRACT:
        return -resolved_amount
    return Decimal("0")
