from decimal import Decimal

from django import forms
from django.forms import BaseInlineFormSet, inlineformset_factory

from apps.rims.finance.models import (
    Budget,
    BudgetLine,
    BudgetYearAllocation,
    DisbursementExecution,
    DisbursementReconciliation,
    DisbursementRequest,
    ExchangeRate,
    Expenditure,
    PaymentBatch,
)
from apps.rims.grants.models import Award
from apps.rims.projects.models import Milestone

_LINE_INPUT = "w-full rounded-lg border border-ruforum-border bg-white px-3 py-2 text-sm text-ruforum-text"


class DisbursementRequestForm(forms.ModelForm):
    class Meta:
        model = DisbursementRequest
        fields = [
            "milestone",
            "amount",
            "payment_method",
            "scheduled_for",
            "account_reference",
            "justification",
            "supporting_document",
        ]
        widgets = {
            "justification": forms.Textarea(attrs={"rows": 4}),
            "scheduled_for": forms.DateInput(attrs={"type": "date"}),
        }

    def __init__(self, *args, **kwargs):
        budget = kwargs.pop("budget", None)
        super().__init__(*args, **kwargs)
        self.fields["amount"].min_value = Decimal("0.01")
        self.fields["milestone"].required = False
        if budget is not None:
            project = getattr(budget.award, "project", None)
            if project:
                self.fields["milestone"].queryset = project.milestones.order_by("due_date")
            else:
                self.fields["milestone"].queryset = Milestone.objects.none()


class BudgetForm(forms.ModelForm):
    class Meta:
        model = Budget
        # PRD §5.4 FRFM001 / FRFM004 — surface multi-year period fields so finance
        # staff can plan a grant that spans 2026→2028 inside a single budget.
        fields = [
            "award",
            "name",
            "fiscal_year",
            "is_multi_year",
            "period_start",
            "period_end",
            "funding_source",
            "status",
            "account_reference",
        ]
        widgets = {
            "period_start": forms.DateInput(attrs={"type": "date"}),
            "period_end": forms.DateInput(attrs={"type": "date"}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["award"].queryset = Award.objects.select_related("application__institution").order_by(
            "-awarded_at"
        )
        self.fields["fiscal_year"].required = False
        self.fields["funding_source"].required = False
        self.fields["status"].required = False
        self.fields["status"].initial = self.instance.status if self.instance.pk else Budget.Status.DRAFT
        self.fields["account_reference"].required = False
        self.fields["is_multi_year"].required = False
        self.fields["period_start"].required = False
        self.fields["period_end"].required = False

    def clean(self):
        cleaned = super().clean()
        if cleaned.get("is_multi_year"):
            if not cleaned.get("period_start") or not cleaned.get("period_end"):
                self.add_error(
                    "period_start",
                    "Multi-year budgets require both a period start and end.",
                )
        return cleaned


class BudgetYearAllocationForm(forms.ModelForm):
    """PRD §5.4 FRFM001 — per-year allocation row on a multi-year budget."""

    class Meta:
        model = BudgetYearAllocation
        fields = ("fiscal_year", "amount", "currency", "notes")
        widgets = {
            "fiscal_year": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "min": "2000", "max": "2100"},
            ),
            "amount": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "step": "0.01", "min": "0"},
            ),
            "currency": forms.Select(attrs={"class": _LINE_INPUT}),
            "notes": forms.TextInput(
                attrs={"class": _LINE_INPUT, "placeholder": "Optional commentary"},
            ),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["notes"].required = False
        self.fields["currency"].required = False


BudgetYearAllocationFormSet = inlineformset_factory(
    Budget,
    BudgetYearAllocation,
    form=BudgetYearAllocationForm,
    extra=3,
    can_delete=True,
)


class ExchangeRateForm(forms.ModelForm):
    """PRD §5.4 FRFM008 — finance-staff editing surface for exchange rates."""

    class Meta:
        model = ExchangeRate
        fields = ("from_currency", "to_currency", "rate", "effective_date", "source")
        widgets = {
            # Searchable Select2 dropdowns (178 ISO-4217 codes). `data-s2` is
            # picked up by components/select2_auto_init.html; _LINE_INPUT keeps
            # a styled native fallback if JS fails to load.
            "from_currency": forms.Select(
                attrs={"class": _LINE_INPUT, "data-s2": True, "data-s2-placeholder": "Search currency…"},
            ),
            "to_currency": forms.Select(
                attrs={"class": _LINE_INPUT, "data-s2": True, "data-s2-placeholder": "Search currency…"},
            ),
            "effective_date": forms.DateInput(attrs={"type": "date"}),
            "rate": forms.NumberInput(
                attrs={"step": "0.00000001", "min": "0"},
            ),
            "source": forms.TextInput(
                attrs={"placeholder": "e.g. BoU 2026-05-18"},
            ),
        }

    def clean(self):
        cleaned = super().clean()
        if cleaned.get("from_currency") == cleaned.get("to_currency"):
            raise forms.ValidationError(
                "From and to currencies must differ."
            )
        rate = cleaned.get("rate")
        if rate is not None and rate <= 0:
            self.add_error("rate", "Rate must be greater than zero.")
        return cleaned


class BudgetLineForm(forms.ModelForm):
    class Meta:
        model = BudgetLine
        # PRD §5.4 FRFM005 — surface funding_source on the line so different
        # rows on the same budget can sit against different donors.
        fields = (
            "label",
            "category",
            "quantity",
            "unit",
            "unit_cost",
            "account_code",
            "allocation_year",
            "amount",
            "funding_source",
        )
        widgets = {
            "label": forms.TextInput(attrs={"class": _LINE_INPUT, "placeholder": "e.g. Personnel, Travel"}),
            "category": forms.Select(attrs={"class": _LINE_INPUT}),
            "quantity": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "step": "0.01", "min": "0", "placeholder": "1.00"},
            ),
            "unit": forms.TextInput(attrs={"class": _LINE_INPUT, "placeholder": "e.g. months, trips, items"}),
            "unit_cost": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "step": "0.01", "min": "0", "placeholder": "0.00"},
            ),
            "account_code": forms.TextInput(attrs={"class": _LINE_INPUT, "placeholder": "Optional finance code"}),
            "allocation_year": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "min": "2000", "max": "2100", "placeholder": "Optional FY"},
            ),
            "amount": forms.NumberInput(
                attrs={"class": _LINE_INPUT, "step": "0.01", "min": "0", "placeholder": "0.00"},
            ),
            "funding_source": forms.Select(attrs={"class": _LINE_INPUT}),
        }

    def clean_amount(self):
        amount = self.cleaned_data.get("amount")
        if amount is not None and amount <= 0:
            raise forms.ValidationError("Amount must be greater than zero.")
        return amount

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["label"].required = False
        self.fields["amount"].required = False
        self.fields["category"].required = False
        self.fields["quantity"].required = False
        self.fields["unit"].required = False
        self.fields["unit_cost"].required = False
        self.fields["account_code"].required = False
        self.fields["allocation_year"].required = False
        self.fields["funding_source"].required = False
        self.fields["funding_source"].empty_label = "— Inherit from budget —"

    def clean(self):
        cleaned = super().clean()
        label = (cleaned.get("label") or "").strip()
        amount = cleaned.get("amount")
        if not label and amount in [None, ""]:
            return cleaned
        if not label:
            self.add_error("label", "This field is required.")
        if amount in [None, ""]:
            self.add_error("amount", "This field is required.")
            return cleaned
        quantity = cleaned.get("quantity") or Decimal("1")
        unit_cost = cleaned.get("unit_cost")
        cleaned["quantity"] = quantity
        if not cleaned.get("category"):
            cleaned["category"] = BudgetLine.Category.OTHER
        if quantity is not None and quantity <= 0:
            self.add_error("quantity", "Quantity must be greater than zero.")
        if unit_cost is not None and unit_cost < 0:
            self.add_error("unit_cost", "Unit cost cannot be negative.")
        if quantity and unit_cost is not None and amount is not None:
            expected = quantity * unit_cost
            if amount != expected:
                cleaned["amount"] = expected
        return cleaned


class _BudgetLineChoiceField(forms.ModelChoiceField):
    """Render budget lines as 'Label — Category · 12,345.00' so finance staff
    can pick the right line at a glance instead of seeing the default
    'BudgetLine object (pk)' fallback."""

    def label_from_instance(self, obj):
        category = obj.get_category_display() if obj.category else ""
        amount = f"{obj.amount:,.2f}" if obj.amount is not None else "—"
        if category:
            return f"{obj.label} — {category} · {amount}"
        return f"{obj.label} · {amount}"


class ExpenditureForm(forms.ModelForm):
    """PRD §5.4 — record an expenditure against a specific budget line.

    When ``budget`` is supplied, the budget_line queryset is restricted to
    that budget, mirroring the DisbursementRequestForm pattern.
    """

    budget_line = _BudgetLineChoiceField(queryset=BudgetLine.objects.none())

    class Meta:
        model = Expenditure
        fields = ["budget_line", "amount", "incurred_on", "description", "receipt"]
        widgets = {
            "incurred_on": forms.DateInput(attrs={"type": "date"}),
            "description": forms.TextInput(attrs={"placeholder": "What was purchased / paid for?"}),
        }

    def __init__(self, *args, **kwargs):
        budget = kwargs.pop("budget", None)
        super().__init__(*args, **kwargs)
        if budget is not None:
            self.fields["budget_line"].queryset = budget.lines.order_by("pk")
        else:
            self.fields["budget_line"].queryset = BudgetLine.objects.all().order_by("pk")
        self.fields["amount"].min_value = Decimal("0.01")

    def clean_amount(self):
        amount = self.cleaned_data.get("amount")
        if amount is not None and amount <= 0:
            raise forms.ValidationError("Amount must be greater than zero.")
        return amount


class DisbursementExecutionForm(forms.ModelForm):
    """PRD §5.4 FRFM039-046 — execution + receipt capture (FRFM045)."""

    class Meta:
        model = DisbursementExecution
        fields = [
            "status",
            "payment_method",
            "scheduled_for",
            "execution_reference",
            "destination_account",
            "notes",
            "proof_of_payment",
            "proof_channel",
        ]
        widgets = {
            "scheduled_for": forms.DateInput(attrs={"type": "date"}),
            "notes": forms.Textarea(attrs={"rows": 3}),
            "execution_reference": forms.TextInput(
                attrs={"placeholder": "Bank reference / mobile-money transaction ID"}
            ),
            # PRD §5.4 FRFM045 — use a plain FileInput rather than the default
            # ClearableFileInput so the template can render its own
            # current-file preview block without competing with Django's
            # built-in "Currently: …  □ Clear  Change:" markup.
            "proof_of_payment": forms.FileInput(
                attrs={"accept": "application/pdf,image/jpeg,image/png"}
            ),
        }
        help_texts = {
            "proof_of_payment": "PDF, JPG, or PNG up to the configured size limit.",
            "proof_channel": "Where this receipt came from.",
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["proof_of_payment"].required = False
        self.fields["proof_channel"].required = False

    def clean(self):
        cleaned = super().clean()
        # PRD §5.4 FRFM041/045 — when an executor marks a payment EXECUTED
        # they must capture the platform reference; the proof file is strongly
        # recommended but not blocked (the executor may attach it shortly
        # after via a follow-up edit). We block on missing reference because
        # without it reconciliation has nothing to match.
        status = cleaned.get("status")
        ref = (cleaned.get("execution_reference") or "").strip()
        if status == DisbursementExecution.Status.EXECUTED and not ref:
            self.add_error(
                "execution_reference",
                "Provide the bank reference or mobile-money transaction ID returned at execution time.",
            )
        return cleaned


class DisbursementReconciliationForm(forms.ModelForm):
    """PRD §5.4 FRFM047/048 — match payments against bank/mobile-money statements."""

    class Meta:
        model = DisbursementReconciliation
        fields = [
            "status",
            "notes",
            "statement_excerpt",
            "statement_source",
            "statement_period_start",
            "statement_period_end",
            "discrepancy_amount",
        ]
        widgets = {
            "notes": forms.Textarea(attrs={"rows": 3}),
            "statement_period_start": forms.DateInput(attrs={"type": "date"}),
            "statement_period_end": forms.DateInput(attrs={"type": "date"}),
            # PRD §5.4 FRFM047 — same rationale as proof_of_payment above.
            "statement_excerpt": forms.FileInput(
                attrs={"accept": "application/pdf,image/jpeg,image/png"}
            ),
        }
        help_texts = {
            "statement_excerpt": "PDF or image of the statement page that evidences this payment.",
            "discrepancy_amount": "Required only when status=Discrepancy (signed; positive = overpaid).",
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for fname in (
            "statement_excerpt",
            "statement_source",
            "statement_period_start",
            "statement_period_end",
            "discrepancy_amount",
        ):
            self.fields[fname].required = False

    def clean(self):
        cleaned = super().clean()
        status = cleaned.get("status")
        # Require a discrepancy amount when the operator flags one.
        if status == DisbursementReconciliation.Status.DISCREPANCY and cleaned.get(
            "discrepancy_amount"
        ) in (None, ""):
            self.add_error(
                "discrepancy_amount",
                "Specify the discrepancy amount when status is set to Discrepancy.",
            )
        # Period sanity check.
        s, e = cleaned.get("statement_period_start"), cleaned.get("statement_period_end")
        if s and e and s > e:
            self.add_error("statement_period_end", "End date must be on or after start date.")
        return cleaned


class BudgetLineBaseFormSet(BaseInlineFormSet):
    def save_new_objects(self, commit=True):
        self.new_objects = []
        for form in self.extra_forms:
            if not form.has_changed():
                continue
            label = (form.cleaned_data.get("label") or "").strip() if hasattr(form, "cleaned_data") else ""
            amount = form.cleaned_data.get("amount") if hasattr(form, "cleaned_data") else None
            if not label and amount in [None, ""]:
                continue
            if self.can_delete and self._should_delete_form(form):
                continue
            self.new_objects.append(self.save_new(form, commit=commit))
        return self.new_objects


BudgetLineFormSet = inlineformset_factory(
    Budget,
    BudgetLine,
    form=BudgetLineForm,
    formset=BudgetLineBaseFormSet,
    extra=0,
    can_delete=True,
    min_num=0,
    validate_min=False,
)


class PaymentBatchForm(forms.ModelForm):
    """PRD §5.4 FRFM040 — header form for a payment batch.

    Items are added by selecting approved DisbursementRequests on the create
    page; this form just captures the batch-level metadata.
    """

    class Meta:
        model = PaymentBatch
        fields = ("batch_reference", "scheduled_for", "payment_method", "notes")
        widgets = {
            "scheduled_for": forms.DateInput(attrs={"type": "date"}),
            "notes": forms.Textarea(attrs={"rows": 3, "placeholder": "Operator notes (optional)"}),
        }
        help_texts = {
            "batch_reference": "Unique reference (e.g. BATCH-2026-04-02-A).",
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["scheduled_for"].required = False
        self.fields["notes"].required = False
