from rest_framework import serializers

from apps.core.tenancy import get_active_organization, assert_same_organization
from .models import Account, Fund, Transaction, JournalEntry, JournalLine, Sale, SalesAccountingSettings


class AccountSerializer(serializers.ModelSerializer):
    """Chart of Accounts entry."""
    parent_label = serializers.SerializerMethodField()

    class Meta:
        model = Account
        fields = [
            "id", "code", "name", "type", "normal_balance", "is_cash",
            "cash_flow_category", "revenue_type", "parent", "parent_label", "expense_class",
            "notes", "is_active", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "parent_label", "created_at", "updated_at"]

    def get_parent_label(self, obj):
        return f"{obj.parent.code} {obj.parent.name}" if obj.parent else None

    def validate_code(self, value):
        return value.strip()

    def validate_parent(self, value):
        # A header cannot roll up into itself, and the tree is one level deep —
        # the report renders group -> lines, with nowhere to put a grandchild.
        if value is None:
            return value
        if self.instance and value.pk == self.instance.pk:
            raise serializers.ValidationError("An account cannot be its own parent.")
        if value.parent_id is not None:
            raise serializers.ValidationError(
                "Parent must be a top-level header account (grouping is one level deep)."
            )
        return value


class FundSerializer(serializers.ModelSerializer):
    """Donor fund / grant. Spent + available are computed, not stored."""
    donor_label = serializers.SerializerMethodField()
    spent_amount = serializers.SerializerMethodField()
    available_amount = serializers.SerializerMethodField()
    project_fund_label = serializers.SerializerMethodField()

    class Meta:
        model = Fund
        fields = [
            "id", "fund_id", "donor", "donor_name", "donor_label", "name",
            "fund_type", "project", "project_fund", "project_fund_label",
            "project_code", "start_date", "end_date",
            "award_amount", "received_amount", "spent_amount", "available_amount",
            "currency", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("donor"), attrs.get("project"), attrs.get("project_fund"))
        return attrs

    def get_project_fund_label(self, obj):
        return str(obj.project_fund) if obj.project_fund_id else None

    def get_donor_label(self, obj):
        if obj.donor_id:
            return getattr(obj.donor, "name", None) or obj.donor_name
        return obj.donor_name

    def get_spent_amount(self, obj):
        # Annotated by the ViewSet's get_queryset; fall back to 0.
        return getattr(obj, "_spent", 0)

    def get_available_amount(self, obj):
        spent = getattr(obj, "_spent", 0) or 0
        return (obj.received_amount or 0) - spent


class TransactionSerializer(serializers.ModelSerializer):
    """Operational transaction. Net IDR recomputed on save."""
    fund_code = serializers.CharField(source="fund.fund_id", read_only=True, default=None)
    account_code = serializers.CharField(source="account.code", read_only=True, default=None)
    account_name = serializers.CharField(source="account.name", read_only=True, default=None)
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True, default=None)
    source_label = serializers.SerializerMethodField()

    class Meta:
        model = Transaction
        fields = [
            "id", "transaction_id", "date", "description", "transaction_type",
            "fund", "fund_code", "donor_name", "project_code", "activity",
            "budget_line", "budget_item", "budget_item_name",
            "source_receivable_invoice", "source_payable_invoice",
            "source_payment", "source_project_fund", "source_label",
            "account", "account_code", "account_name",
            "counterparty", "currency", "original_amount", "exchange_rate",
            "debit_idr", "credit_idr", "net_expense_idr", "status",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "net_expense_idr", "created_at", "updated_at"]

    def get_source_label(self, obj):
        # Human-readable name of whichever source document is linked.
        for attr in ("source_receivable_invoice", "source_payable_invoice",
                     "source_payment", "source_project_fund"):
            val = getattr(obj, attr, None)
            if val is not None:
                return str(val)
        return None


class JournalLineSerializer(serializers.ModelSerializer):
    """A debit/credit line within a journal voucher."""
    account_code = serializers.CharField(source="account.code", read_only=True)
    account_name = serializers.CharField(source="account.name", read_only=True)

    class Meta:
        model = JournalLine
        fields = [
            "id", "account", "account_code", "account_name", "description",
            "debit_idr", "credit_idr", "created_at",
        ]
        read_only_fields = ["id", "created_at"]

    def validate(self, attrs):
        debit = attrs.get("debit_idr") or 0
        credit = attrs.get("credit_idr") or 0
        if debit < 0 or credit < 0:
            raise serializers.ValidationError("debit_idr and credit_idr cannot be negative.")
        if debit and credit:
            raise serializers.ValidationError("A line cannot have both a debit and a credit.")
        if not debit and not credit:
            raise serializers.ValidationError("A line must have either a debit or a credit.")
        return attrs


class JournalEntrySerializer(serializers.ModelSerializer):
    """Journal voucher with nested lines. Writeable lines on create/update.

    `status` is read-only here — posting only happens through the
    `post_entry` action, which enforces the balance check. This keeps a
    Posted journal from ever being written by a plain PATCH.
    """
    lines = JournalLineSerializer(many=True)
    total_debit = serializers.SerializerMethodField()
    total_credit = serializers.SerializerMethodField()
    is_balanced = serializers.SerializerMethodField()
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True, default=None)
    source_label = serializers.SerializerMethodField()

    class Meta:
        model = JournalEntry
        fields = [
            "id", "journal_no", "date", "reference", "description", "fund",
            "donor_name", "project_code", "activity", "budget_line",
            "budget_item", "budget_item_name", "source_transaction",
            "source_receivable_invoice", "source_payable_invoice",
            "source_payment", "source_project_fund", "source_label", "status",
            "lines", "total_debit", "total_credit", "is_balanced",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "status", "created_at", "updated_at"]

    def get_source_label(self, obj):
        for attr in ("source_transaction", "source_receivable_invoice",
                     "source_payable_invoice", "source_payment", "source_project_fund"):
            val = getattr(obj, attr, None)
            if val is not None:
                return str(val)
        return None

    def get_total_debit(self, obj):
        debit, _ = obj.totals()
        return debit

    def get_total_credit(self, obj):
        _, credit = obj.totals()
        return credit

    def get_is_balanced(self, obj):
        debit, credit = obj.totals()
        return debit == credit

    def validate_lines(self, value):
        if len(value) < 2:
            raise serializers.ValidationError("A journal entry needs at least 2 lines.")
        return value

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            org = get_active_organization(request)
            assert_same_organization(
                org, attrs.get("fund"), attrs.get("source_transaction"),
                attrs.get("source_receivable_invoice"), attrs.get("source_payable_invoice"),
                attrs.get("source_payment"), attrs.get("source_project_fund"),
            )
            budget_item = attrs.get("budget_item")
            if budget_item is not None and budget_item.category.organization_id != org.id:
                raise serializers.ValidationError({"budget_item": "Outside your organization."})
            for line in attrs.get("lines", []):
                account = line.get("account")
                if account is not None and account.organization_id != org.id:
                    raise serializers.ValidationError({"lines": "An account is outside your organization."})
        return attrs

    def create(self, validated_data):
        lines = validated_data.pop("lines", [])
        entry = JournalEntry.objects.create(**validated_data)
        for line in lines:
            JournalLine.objects.create(journal=entry, **line)
        return entry

    def update(self, instance, validated_data):
        lines = validated_data.pop("lines", None)
        if lines is not None and instance.status == "Posted":
            raise serializers.ValidationError(
                {"lines": "Cannot edit lines on a Posted journal. Reject it first to reopen for edits."}
            )
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        if lines is not None:
            instance.lines.all().delete()
            for line in lines:
                JournalLine.objects.create(journal=instance, **line)
        return instance


class SaleSerializer(serializers.ModelSerializer):
    """A book/accessory sale. `status`/posting FKs are read-only; posting only
    happens through the `post_sale` action, which creates the stock movement
    and journal entry together."""
    item_name = serializers.CharField(source="item.stock_item.name", read_only=True)
    buyer_contact_name = serializers.CharField(source="buyer_contact.name", read_only=True, default=None)

    class Meta:
        model = Sale
        fields = [
            "id", "sale_no", "date", "item", "item_name", "quantity", "unit_price",
            "total_amount", "buyer_name", "buyer_contact", "buyer_contact_name",
            "stock_movement", "journal_entry", "status", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "total_amount", "stock_movement", "journal_entry", "status",
            "created_at", "updated_at",
        ]

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            org = get_active_organization(request)
            item = attrs.get("item")
            if item is not None and item.stock_item.organization_id != org.id:
                raise serializers.ValidationError({"item": "Outside your organization."})
            assert_same_organization(org, attrs.get("buyer_contact"))
        return attrs

    def create(self, validated_data):
        sale = Sale(**validated_data)
        sale.recompute_total()
        sale.save()
        return sale

    def update(self, instance, validated_data):
        if instance.status == "Posted":
            raise serializers.ValidationError("Cannot edit a Posted sale.")
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.recompute_total()
        instance.save()
        return instance


class SalesAccountingSettingsSerializer(serializers.ModelSerializer):
    class Meta:
        model = SalesAccountingSettings
        fields = [
            "id", "cash_account", "revenue_account", "cogs_account", "inventory_account",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            org = get_active_organization(request)
            assert_same_organization(
                org, attrs.get("cash_account"), attrs.get("revenue_account"),
                attrs.get("cogs_account"), attrs.get("inventory_account"),
            )
        return attrs
