from __future__ import annotations

from dataclasses import dataclass, field
from datetime import date
import calendar
from decimal import Decimal
from typing import Iterable

from .enums import (
    AmountMode,
    BaseEffect,
    CalculationPhase,
    CategoryNature,
    EmploymentTaxMode,
    InputMode,
    PayslipSide,
)
from .money import money


@dataclass(frozen=True)
class EmployerProfile:
    name: str
    tin: str | None = None
    currency: str = "UGX"


@dataclass(frozen=True)
class EmployeeProfile:
    full_name: str
    staff_number: str | None = None
    tin: str | None = None
    nin: str | None = None
    nssf_number: str | None = None
    department: str | None = None
    position: str | None = None
    bank_account_number: str | None = None
    employment_tax_mode: EmploymentTaxMode = EmploymentTaxMode.PRIMARY_RESIDENT


@dataclass(frozen=True)
class PayrollPeriod:
    year: int
    month: int
    start_date: date
    end_date: date
    label: str

    @classmethod
    def from_month(cls, year: int, month: int) -> "PayrollPeriod":
        days = calendar.monthrange(year, month)[1]
        start = date(year, month, 1)
        end = date(year, month, days)
        label = f"{start.strftime('%B')} {year}"
        return cls(year=year, month=month, start_date=start, end_date=end, label=label)


@dataclass(frozen=True)
class PaymentCategory:
    code: str
    name: str
    nature: CategoryNature
    payslip_side: PayslipSide
    phase: CalculationPhase
    cash_effect: BaseEffect
    paye_base_effect: BaseEffect
    nssf_employee_base_effect: BaseEffect
    nssf_employer_base_effect: BaseEffect
    employer_cost_effect: BaseEffect = BaseEffect.IGNORE
    show_on_payslip: bool = True
    description: str = ""

    @classmethod
    def from_nature(
        cls,
        *,
        code: str,
        name: str,
        nature: str | CategoryNature,
        attracts_paye: bool = False,
        attracts_nssf: bool = False,
        payslip_side: PayslipSide | str | None = None,
        phase: CalculationPhase | str | None = None,
        cash_effect: BaseEffect | str | None = None,
        paye_base_effect: BaseEffect | str | None = None,
        nssf_employee_base_effect: BaseEffect | str | None = None,
        nssf_employer_base_effect: BaseEffect | str | None = None,
        employer_cost_effect: BaseEffect | str | None = None,
        description: str = "",
    ) -> "PaymentCategory":
        resolved_nature = CategoryNature(nature)
        default_side_map = {
            CategoryNature.INCREMENT: PayslipSide.EARNINGS,
            CategoryNature.DEDUCTION: PayslipSide.DEDUCTIONS,
            CategoryNature.INCREMENT2: PayslipSide.EARNINGS,
            CategoryNature.DO_NOTHING: PayslipSide.MEMO,
            CategoryNature.BONUS: PayslipSide.EARNINGS,
        }
        default_phase_map = {
            CategoryNature.INCREMENT: CalculationPhase.PRE_TAX,
            CategoryNature.DEDUCTION: CalculationPhase.POST_TAX,
            CategoryNature.INCREMENT2: CalculationPhase.POST_TAX,
            CategoryNature.DO_NOTHING: CalculationPhase.PRE_TAX,
            CategoryNature.BONUS: CalculationPhase.PRE_TAX,
        }
        default_cash_effect_map = {
            CategoryNature.INCREMENT: BaseEffect.ADD,
            CategoryNature.DEDUCTION: BaseEffect.SUBTRACT,
            CategoryNature.INCREMENT2: BaseEffect.ADD,
            CategoryNature.DO_NOTHING: BaseEffect.IGNORE,
            CategoryNature.BONUS: BaseEffect.ADD,
        }

        resolved_side = PayslipSide(payslip_side) if payslip_side else default_side_map[resolved_nature]
        resolved_phase = CalculationPhase(phase) if phase else default_phase_map[resolved_nature]
        resolved_cash_effect = BaseEffect(cash_effect) if cash_effect else default_cash_effect_map[resolved_nature]

        default_paye_effect = BaseEffect.ADD if attracts_paye else BaseEffect.IGNORE
        if resolved_cash_effect == BaseEffect.SUBTRACT and paye_base_effect is None:
            default_paye_effect = BaseEffect.IGNORE

        default_nssf_effect = BaseEffect.ADD if attracts_nssf else BaseEffect.IGNORE

        return cls(
            code=code,
            name=name,
            nature=resolved_nature,
            payslip_side=resolved_side,
            phase=resolved_phase,
            cash_effect=resolved_cash_effect,
            paye_base_effect=BaseEffect(paye_base_effect) if paye_base_effect else default_paye_effect,
            nssf_employee_base_effect=BaseEffect(nssf_employee_base_effect) if nssf_employee_base_effect else default_nssf_effect,
            nssf_employer_base_effect=BaseEffect(nssf_employer_base_effect) if nssf_employer_base_effect else default_nssf_effect,
            employer_cost_effect=BaseEffect(employer_cost_effect) if employer_cost_effect else BaseEffect.IGNORE,
            description=description,
        )

    @classmethod
    def from_hrm(
        cls,
        *,
        code: str,
        name: str,
        nature: str,
        taxable: bool = False,
        nssfable: bool = False,
        description: str = "",
    ) -> "PaymentCategory":
        """Create a category using the same simple fields as the HRM Django model."""
        return cls.from_nature(
            code=code,
            name=name,
            nature=nature,
            attracts_paye=taxable,
            attracts_nssf=nssfable,
            description=description,
        )

    # ── Preset factory classmethods for common Uganda allowances ──

    @classmethod
    def overtime(cls, code: str = "overtime", name: str = "Overtime") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Increment", attracts_paye=True, attracts_nssf=True)

    @classmethod
    def transport(cls, code: str = "transport", name: str = "Transport Allowance") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Increment", attracts_paye=False, attracts_nssf=False)

    @classmethod
    def housing_benefit(cls, code: str = "housing_benefit", name: str = "Housing Benefit") -> "PaymentCategory":
        return cls.from_nature(
            code=code, name=name, nature="Do nothing",
            attracts_paye=True, attracts_nssf=False,
            payslip_side=PayslipSide.MEMO, cash_effect=BaseEffect.IGNORE,
        )

    @classmethod
    def sacco_deduction(cls, code: str = "sacco", name: str = "SACCO Deduction") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Deduction", attracts_paye=False, attracts_nssf=False)

    @classmethod
    def salary_advance(cls, code: str = "salary_advance", name: str = "Salary Advance") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Deduction", attracts_paye=False, attracts_nssf=False)

    @classmethod
    def bonus(cls, code: str = "bonus", name: str = "Bonus") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Bonus", attracts_paye=True, attracts_nssf=True)

    @classmethod
    def payroll_correction(cls, code: str = "payroll_correction", name: str = "Payroll Correction") -> "PaymentCategory":
        """Reduces chargeable income (PAYE base) but not NSSF base."""
        return cls.from_nature(
            code=code, name=name, nature="Increment",
            attracts_paye=True, attracts_nssf=False,
            paye_base_effect=BaseEffect.SUBTRACT,
            cash_effect=BaseEffect.IGNORE,
        )

    @classmethod
    def medical_insurance(cls, code: str = "medical_insurance", name: str = "Medical Insurance") -> "PaymentCategory":
        return cls.from_nature(
            code=code, name=name, nature="Do nothing",
            attracts_paye=False, attracts_nssf=False,
            payslip_side=PayslipSide.EMPLOYER_CONTRIBUTIONS,
            cash_effect=BaseEffect.IGNORE,
            employer_cost_effect=BaseEffect.ADD,
        )

    @classmethod
    def gratuity(cls, code: str = "gratuity", name: str = "Gratuity") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Increment2", attracts_paye=False, attracts_nssf=False)

    @classmethod
    def pension_deduction(cls, code: str = "pension", name: str = "Pension Deduction") -> "PaymentCategory":
        """Pre-tax deduction that reduces PAYE base."""
        return cls.from_nature(
            code=code, name=name, nature="Increment",
            phase=CalculationPhase.PRE_TAX,
            cash_effect=BaseEffect.SUBTRACT,
            paye_base_effect=BaseEffect.SUBTRACT,
            attracts_nssf=False,
        )

    @classmethod
    def notice_pay(cls, code: str = "notice_pay", name: str = "Notice Pay") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Increment", attracts_paye=True, attracts_nssf=True)

    @classmethod
    def leave_compensation(cls, code: str = "leave_compensation", name: str = "Leave Compensation") -> "PaymentCategory":
        return cls.from_nature(code=code, name=name, nature="Increment", attracts_paye=True, attracts_nssf=True)


@dataclass(frozen=True)
class PayrollLineItem:
    category: PaymentCategory
    amount: Decimal | int | float | str
    amount_mode: AmountMode = AmountMode.FIXED
    reference: str | None = None

    @property
    def normalized_amount(self) -> Decimal:
        return money(self.amount)


@dataclass(frozen=True)
class PayrollRequest:
    employer: EmployerProfile
    employee: EmployeeProfile
    period: PayrollPeriod
    input_mode: InputMode
    target_amount: Decimal | int | float | str
    items: tuple[PayrollLineItem, ...] = field(default_factory=tuple)

    def __post_init__(self) -> None:
        object.__setattr__(self, "input_mode", InputMode(self.input_mode))
        object.__setattr__(self, "items", tuple(self.items))

    @property
    def normalized_target_amount(self) -> Decimal:
        return money(self.target_amount)


@dataclass(frozen=True)
class ResultLineItem:
    category: PaymentCategory
    amount: Decimal
    cash_impact: Decimal
    paye_base_impact: Decimal
    nssf_employee_base_impact: Decimal
    nssf_employer_base_impact: Decimal
    employer_cost_impact: Decimal


@dataclass(frozen=True)
class PayrollResult:
    employer: EmployerProfile
    employee: EmployeeProfile
    period: PayrollPeriod
    basic_pay: Decimal
    gross_earnings: Decimal
    gross_cash_before_statutory: Decimal
    chargeable_income: Decimal
    paye_base: Decimal
    total_paye: Decimal
    lst: Decimal
    nssf_employee: Decimal
    nssf_employer: Decimal
    total_deductions: Decimal
    net_pay: Decimal
    total_employer_cost: Decimal
    items: tuple[ResultLineItem, ...]

    def iter_side(self, side: PayslipSide | str) -> Iterable[ResultLineItem]:
        resolved_side = PayslipSide(side)
        return tuple(item for item in self.items if item.category.payslip_side == resolved_side and item.category.show_on_payslip)


@dataclass(frozen=True)
class PayrollBatchRequest:
    employer: EmployerProfile
    period: PayrollPeriod
    employee_requests: tuple[PayrollRequest, ...]


@dataclass(frozen=True)
class PayrollBatchResult:
    employer: EmployerProfile
    period: PayrollPeriod
    results: tuple[PayrollResult, ...]
    total_basic_pay: Decimal
    total_gross_earnings: Decimal
    total_nssf_employee: Decimal
    total_nssf_employer: Decimal
    total_paye: Decimal
    total_lst: Decimal
    total_net_pay: Decimal
    total_employer_cost: Decimal
