from rest_framework import serializers
from .models import (
    Project, ProjectDocument, ProjectTeamMember, Deliverable, SubmissionVersion,
    ProjectIndicator, IndicatorRecord, ProjectRisk,
    ProjectFinance, ProjectDissemination, ProjectSchedule, ProjectFund,
    ProjectFundAllocation, ProjectFundAllocationRevision, ProjectSettings, CashAdvance,
    FundingOpportunity, FundingProposal, FundReport,
    ProjectFundTranche, ProjectFundCompliance, FundDocument,
    ProjectResource, ProjectLog, PaymentRequestForm, PRFAttachment,
)


class ProjectSettingsSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProjectSettings
        fields = [
            "access_level", "donor_pic_can_access",
            "proposal_template", "report_template", "updated_at",
        ]
        read_only_fields = ["updated_at"]


class ProjectSerializer(serializers.ModelSerializer):
    """Serializer for Project model."""
    lead_name = serializers.SerializerMethodField()
    fund_count = serializers.SerializerMethodField()
    fund_totals = serializers.SerializerMethodField()
    fund_donors = serializers.SerializerMethodField()
    department_names = serializers.SerializerMethodField()

    class Meta:
        model = Project
        fields = [
            "id", "name", "project_number", "description", "status", "lead", "lead_name",
            "departments", "department_names",
            "start_date", "end_date", "tags", "display_currency",
            "fund_count", "fund_totals", "fund_donors",
            "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "project_number", "department_names",
            "created_at", "updated_at", "fund_count", "fund_totals", "fund_donors",
        ]

    def validate_departments(self, value):
        """Only research departments may own a project."""
        non_research = [d.name for d in value if not d.is_research]
        if non_research:
            raise serializers.ValidationError(
                f"Not a research department: {', '.join(non_research)}."
            )
        return value

    def get_department_names(self, obj) -> list:
        # departments is prefetched on the ViewSet.
        return [d.name for d in obj.departments.all()]

    def create(self, validated_data):
        # project_number derives from the first department, so it can only be
        # assigned after the M2M is written (ModelSerializer.create does that).
        project = super().create(validated_data)
        project.assign_project_number()
        return project

    def get_lead_name(self, obj):
        if not obj.lead_id:
            return None
        user = getattr(obj.lead, "user", None)
        if user is None:
            return None
        return user.get_full_name() or user.username or user.email

    def get_fund_count(self, obj) -> int:
        # funds is prefetched on the ViewSet; len() reads the cache instead of
        # issuing a COUNT query per row.
        return len(obj.funds.all())

    def get_fund_totals(self, obj) -> dict:
        totals: dict[str, float] = {}
        for f in obj.funds.all():
            totals[f.currency] = float(totals.get(f.currency, 0)) + float(f.amount)
        return totals

    # Funding is "secured" once it reaches pledged or later. Prospect/proposal
    # funds are not yet committed money.
    _SECURED_STATUSES = {"pledged", "partially_received", "received", "allocated", "closed"}

    def _fund_donor_name(self, f):
        donor = getattr(f, "donor", None)
        # Prefer the canonical donor record; fall back to the free-text source.
        return (getattr(donor, "name", None) if donor else None) or (f.source or None)

    def get_fund_donors(self, obj) -> list:
        # Once any fund is secured, the project's donors come from the secured
        # funds only. Before that, show the prospect/proposal donors so the
        # pipeline is still visible.
        funds = list(obj.funds.all())
        secured = [f for f in funds if f.status in self._SECURED_STATUSES]
        relevant = secured if secured else funds

        names: list[str] = []
        for f in relevant:
            name = self._fund_donor_name(f)
            if name and name not in names:
                names.append(name)
        return names

    def validate_lead(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class ProjectDocumentSerializer(serializers.ModelSerializer):
    """Serializer for ProjectDocument model."""
    project_name = serializers.SerializerMethodField()
    uploaded_by_name = serializers.SerializerMethodField()
    uploaded_by_email = serializers.SerializerMethodField()
    file_url = serializers.SerializerMethodField()
    file_size = serializers.SerializerMethodField()
    file_ext = serializers.SerializerMethodField()
    allowed_user_names = serializers.SerializerMethodField()

    class Meta:
        model = ProjectDocument
        fields = [
            "id", "project", "project_name",
            "name", "file", "file_url", "file_size", "file_ext",
            "uploaded_by", "uploaded_by_name", "uploaded_by_email",
            "access_level", "allowed_users", "allowed_user_names",
            "created_at",
        ]
        read_only_fields = [
            "id", "created_at", "file_url", "file_size", "file_ext",
            "project_name",
            "uploaded_by_name", "uploaded_by_email", "allowed_user_names",
        ]

    def validate_file(self, value):
        from apps.core.uploads import validate_upload
        if value:
            validate_upload(value)
        return value

    def get_project_name(self, obj):
        return getattr(obj.project, "name", None) if obj.project_id else None

    def get_uploaded_by_email(self, obj):
        return getattr(obj.uploaded_by, "email", None) if obj.uploaded_by_id else None

    def get_uploaded_by_name(self, obj):
        if obj.uploaded_by:
            return obj.uploaded_by.get_full_name() or obj.uploaded_by.username
        return None

    def get_file_url(self, obj):
        if not obj.file:
            return None
        request = self.context.get("request")
        url = obj.file.url
        return request.build_absolute_uri(url) if request else url

    def get_file_size(self, obj):
        if not obj.file:
            return None
        try:
            return obj.file.size
        except (FileNotFoundError, ValueError, OSError):
            return None

    def get_file_ext(self, obj):
        if not obj.file:
            return None
        name = obj.file.name or ""
        _, _, ext = name.rpartition(".")
        return ext.lower() if ext and ext != name else None

    def get_allowed_user_names(self, obj):
        return [
            (u.get_full_name() or u.username or u.email)
            for u in obj.allowed_users.all()
        ]

    def validate_project(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class ProjectTeamMemberSerializer(serializers.ModelSerializer):
    """Serializer for ProjectTeamMember model."""
    user_name = serializers.SerializerMethodField()
    project_name = serializers.SerializerMethodField()

    class Meta:
        model = ProjectTeamMember
        fields = ["id", "project", "project_name", "user", "user_name", "role", "joined_at"]
        read_only_fields = ["id", "joined_at"]

    def get_user_name(self, obj):
        return obj.user.get_full_name() or obj.user.username

    def get_project_name(self, obj):
        return obj.project.name

    def validate_project(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class ProjectFinanceSerializer(serializers.ModelSerializer):
    project_id = serializers.CharField(source="project.id", read_only=True)
    ledger_label = serializers.CharField(source="get_ledger_display", read_only=True)
    budget_item_code = serializers.CharField(source="budget_item.code", read_only=True, default=None)
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True, default=None)
    budget_item_category = serializers.CharField(source="budget_item.category.name", read_only=True, default=None)
    budget_category_code = serializers.CharField(source="budget_category.code", read_only=True, default=None)
    budget_category_name = serializers.CharField(source="budget_category.name", read_only=True, default=None)
    receipt_url = serializers.SerializerMethodField()
    # Amount converted to a target currency (context["to_currency"]) at the
    # rate on the entry's date. Used to show project-fund-currency totals.
    amount_converted = serializers.SerializerMethodField()
    converted_currency = serializers.SerializerMethodField()
    approval_status_label = serializers.CharField(source="get_approval_status_display", read_only=True)
    approved_by_name = serializers.SerializerMethodField()
    created_by_name = serializers.SerializerMethodField()
    amount_idr_effective = serializers.SerializerMethodField()

    class Meta:
        model = ProjectFinance
        fields = [
            "id", "project", "project_id", "description", "amount", "currency", "amount_idr", "amount_idr_effective", "type", "ledger", "ledger_label",
            "category", "budget_category", "budget_category_code", "budget_category_name",
            "fund", "budget_item", "budget_item_code", "budget_item_name", "budget_item_category",
            "date", "receipt", "receipt_url", "amount_converted", "converted_currency",
            "approval_status", "approval_status_label", "approved_by", "approved_by_name", "approved_at",
            "created_by", "created_by_name", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "created_at", "updated_at", "project_id", "ledger_label", "amount_idr", "amount_idr_effective",
            "budget_category_code", "budget_category_name",
            "budget_item_code", "budget_item_name", "budget_item_category",
            "receipt_url", "amount_converted", "converted_currency",
            "approval_status", "approval_status_label", "approved_by", "approved_by_name", "approved_at",
            "created_by", "created_by_name",
        ]
        extra_kwargs = {"receipt": {"write_only": True, "required": False}}

    def validate(self, attrs):
        """Resolve `fund` when it is unambiguous, and keep it consistent with
        `project`.

        Donor spend attribution reads this field; an entry that lands on the
        wrong fund shows one donor another donor's money. On a project with a
        single fund there is exactly one candidate, so it is filled in rather
        than made the caller's problem. On a co-funded project it must be
        chosen explicitly — the answer is not derivable there.
        """
        attrs = super().validate(attrs)
        project = attrs.get("project") or getattr(self.instance, "project", None)
        fund = attrs.get("fund", getattr(self.instance, "fund", None))

        if fund is not None:
            if project is not None and fund.project_id != project.id:
                raise serializers.ValidationError(
                    {"fund": "That fund does not belong to this project."}
                )
            return attrs

        if project is not None and "fund" not in attrs and self.instance is None:
            candidates = list(project.funds.all()[:2])
            if len(candidates) == 1:
                attrs["fund"] = candidates[0]
        return attrs

    def get_receipt_url(self, obj):
        if not obj.receipt:
            return None
        request = self.context.get("request")
        url = obj.receipt.url
        return request.build_absolute_uri(url) if request else url

    def get_amount_idr_effective(self, obj):
        """obj.amount_idr if already stamped, else a live-fetched rate — so
        entries whose creation-time FX lookup failed (or predate stamping)
        still convert instead of being stuck null forever."""
        if obj.amount_idr is not None:
            return obj.amount_idr
        from .fx import fetch_idr_rate
        ref_date = obj.date or (obj.created_at.date() if obj.created_at else None)
        if ref_date is None:
            return None
        rate = fetch_idr_rate(obj.currency, ref_date)
        return (obj.amount * rate) if rate is not None else None

    def get_converted_currency(self, obj):
        return (self.context.get("to_currency") or "").upper() or None

    def get_amount_converted(self, obj):
        from .fx import convert_on_date
        to_ccy = (self.context.get("to_currency") or "").upper()
        if not to_ccy:
            return None
        if (obj.currency or "").upper() == to_ccy:
            return str(obj.amount)
        ref_date = obj.date or (obj.created_at.date() if obj.created_at else None)
        val = convert_on_date(obj.amount, obj.currency, to_ccy, ref_date)
        return str(val) if val is not None else None

    def get_approved_by_name(self, obj):
        if not obj.approved_by_id:
            return None
        return obj.approved_by.get_full_name() or obj.approved_by.email

    def get_created_by_name(self, obj):
        if not obj.created_by_id:
            return None
        return obj.created_by.get_full_name() or obj.created_by.email

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            from apps.core.tenancy import get_active_organization, assert_same_organization
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("project"), attrs.get("budget_category"))
            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."})
        return attrs


class CashAdvanceSerializer(serializers.ModelSerializer):
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    recipient_name = serializers.SerializerMethodField()
    budget_item_code = serializers.CharField(source="budget_item.code", read_only=True, default=None)
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True, default=None)
    settlement_receipt_url = serializers.SerializerMethodField()
    return_receipt_url = serializers.SerializerMethodField()
    outstanding_amount = serializers.SerializerMethodField()
    settled_by_name = serializers.SerializerMethodField()
    created_by_name = serializers.SerializerMethodField()

    class Meta:
        model = CashAdvance
        fields = [
            "id", "project", "fund", "recipient", "recipient_name", "purpose",
            "budget_item", "budget_item_code", "budget_item_name",
            "amount", "currency", "amount_idr", "date_issued",
            "status", "status_label", "outstanding_amount",
            "spent_amount", "returned_amount",
            "settlement_receipt", "settlement_receipt_url",
            "return_receipt", "return_receipt_url", "settlement_notes",
            "settled_at", "settled_by", "settled_by_name",
            "created_by", "created_by_name", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "amount_idr", "status", "status_label", "outstanding_amount",
            "spent_amount", "returned_amount", "settlement_receipt_url",
            "return_receipt_url",
            "settlement_notes", "settled_at", "settled_by", "settled_by_name",
            "created_by", "created_by_name", "created_at", "updated_at",
            "budget_item_code", "budget_item_name",
        ]
        extra_kwargs = {
            "settlement_receipt": {"write_only": True, "required": False},
            "return_receipt": {"write_only": True, "required": False},
        }

    def get_recipient_name(self, obj):
        if not obj.recipient_id:
            return None
        return str(obj.recipient)

    def get_settlement_receipt_url(self, obj):
        if not obj.settlement_receipt:
            return None
        request = self.context.get("request")
        url = obj.settlement_receipt.url
        return request.build_absolute_uri(url) if request else url

    def get_return_receipt_url(self, obj):
        if not obj.return_receipt:
            return None
        request = self.context.get("request")
        url = obj.return_receipt.url
        return request.build_absolute_uri(url) if request else url

    def get_outstanding_amount(self, obj):
        if obj.status in ("settled", "cancelled"):
            return "0"
        remaining = obj.amount - (obj.spent_amount or 0) - (obj.returned_amount or 0)
        return str(remaining)

    def get_settled_by_name(self, obj):
        if not obj.settled_by_id:
            return None
        return obj.settled_by.get_full_name() or obj.settled_by.email

    def get_created_by_name(self, obj):
        if not obj.created_by_id:
            return None
        return obj.created_by.get_full_name() or obj.created_by.email

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            from apps.core.tenancy import get_active_organization, assert_same_organization
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("project"), attrs.get("fund"), attrs.get("recipient"))
            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."})
        return attrs


class ProjectDisseminationSerializer(serializers.ModelSerializer):
    project_id = serializers.CharField(source="project.id", read_only=True)
    event_name = serializers.SerializerMethodField()
    publication_title = serializers.SerializerMethodField()

    class Meta:
        model = ProjectDissemination
        fields = [
            "id", "project", "project_id", "title", "description", "date", "location",
            "event", "event_name", "publication", "publication_title",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at", "project_id", "event_name", "publication_title"]

    def get_event_name(self, obj):
        return obj.event.name if obj.event else None

    def get_publication_title(self, obj):
        return obj.publication.title if obj.publication else None

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            from apps.core.tenancy import get_active_organization, assert_same_organization
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("project"), attrs.get("event"), attrs.get("publication"))
        return attrs


class ProjectResourceSerializer(serializers.ModelSerializer):
    project_id = serializers.CharField(source="project.id", read_only=True)
    created_by_name = serializers.SerializerMethodField()

    class Meta:
        model = ProjectResource
        fields = [
            "id", "project", "project_id", "kind", "title", "link",
            "detail", "source", "year",
            "authors", "publisher", "lit_type", "quoted_page", "notes",
            "created_by", "created_by_name", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "project", "project_id", "created_by", "created_by_name",
            "created_at", "updated_at",
        ]

    def get_created_by_name(self, obj):
        if not obj.created_by_id:
            return None
        return obj.created_by.get_full_name() or obj.created_by.email


class ProjectLogSerializer(serializers.ModelSerializer):
    actor_name = serializers.SerializerMethodField()

    class Meta:
        model = ProjectLog
        fields = [
            "id", "project", "log_type", "action", "detail",
            "actor", "actor_name", "created_at",
        ]
        read_only_fields = fields

    def get_actor_name(self, obj):
        if not obj.actor_id:
            return None
        return obj.actor.get_full_name() or obj.actor.email


class ProjectScheduleSerializer(serializers.ModelSerializer):
    project_id = serializers.CharField(source="project.id", read_only=True)
    status_label = serializers.CharField(source="get_status_display", read_only=True)

    class Meta:
        model = ProjectSchedule
        fields = [
            "id", "project", "project_id", "title", "description", "start_date", "end_date",
            "status", "status_label", "completed_date", "revised_end_date", "donor_visible",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at", "project_id", "status_label"]


class ProjectFundSerializer(serializers.ModelSerializer):
    project = serializers.PrimaryKeyRelatedField(
        queryset=Project.objects.all(), required=False, allow_null=True,
    )
    project_id = serializers.CharField(source="project.id", read_only=True, default=None)
    project_name = serializers.CharField(source="project.name", read_only=True, default=None)
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    fund_type_label = serializers.CharField(source="get_fund_type_display", read_only=True)
    donor_id = serializers.CharField(source="donor.id", read_only=True, default=None)
    donor_name = serializers.CharField(source="donor.name", read_only=True, default=None)
    donor_abbreviation = serializers.CharField(source="donor.abbreviation", read_only=True, default=None)
    donor_logo_url = serializers.SerializerMethodField()
    donor_website = serializers.CharField(source="donor.website", read_only=True, default=None)

    class Meta:
        model = ProjectFund
        fields = [
            "id", "project", "project_id", "project_name",
            "source", "donor", "donor_id", "donor_name", "donor_abbreviation", "donor_logo_url", "donor_website",
            "fund_type", "fund_type_label", "fund_code", "grant_number",
            "amount", "currency", "exchange_rate", "amount_idr", "conversion_timing", "status", "status_label",
            "frozen", "agreement_date", "received_date", "notes",
            "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "created_at", "updated_at", "project_id", "project_name",
            "status_label", "fund_type_label", "fund_code", "donor_id", "donor_name", "donor_abbreviation", "donor_logo_url", "donor_website",
            "exchange_rate", "amount_idr", "frozen",
        ]

    def get_donor_logo_url(self, obj):
        if not obj.donor or not obj.donor.logo:
            return None
        request = self.context.get("request")
        url = obj.donor.logo.url
        return request.build_absolute_uri(url) if request else url

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


class ProjectFundAllocationSerializer(serializers.ModelSerializer):
    """Allocation of a fund's amount to a budget item."""
    budget_item_code = serializers.CharField(source="budget_item.code", read_only=True)
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True)
    category_name = serializers.CharField(source="budget_item.category.name", read_only=True, default=None)
    fund_source = serializers.CharField(source="fund.source", read_only=True, default=None)
    fund_currency = serializers.CharField(source="fund.currency", read_only=True, default=None)
    fund_exchange_rate = serializers.DecimalField(source="fund.exchange_rate", max_digits=18, decimal_places=6, read_only=True, default=None)
    fund_exchange_rate_effective = serializers.SerializerMethodField()
    fund_donor_name = serializers.SerializerMethodField()

    def get_fund_exchange_rate_effective(self, obj):
        """fund.exchange_rate if the fund has one (frozen on received_date), else
        a live rate today — so allocations on a not-yet-received fund still
        convert to IDR instead of being stuck unconvertible."""
        if not obj.fund_id:
            return None
        if obj.fund.exchange_rate is not None:
            return obj.fund.exchange_rate
        from django.utils import timezone
        from .fx import fetch_idr_rate
        return fetch_idr_rate(obj.fund.currency, timezone.now().date())

    class Meta:
        model = ProjectFundAllocation
        fields = [
            "id", "fund", "fund_source", "fund_currency", "fund_exchange_rate", "fund_exchange_rate_effective", "fund_donor_name",
            "budget_item", "budget_item_code", "budget_item_name", "category_name",
            "unit", "quantity", "frequency",
            "unit_cost", "unit_cost_currency", "unit_cost_fund_ccy",
            "time_allocated_pct", "amount", "note", "version", "created_at", "updated_at",
        ]
        # Read-only view: all changes go through ProjectFundAllocationRevision
        # (see ProjectFundAllocationViewSet), so nothing here is writable via
        # this serializer.
        read_only_fields = fields

    def get_fund_donor_name(self, obj):
        f = obj.fund
        if f and f.donor_id:
            return f.donor.name
        return (f.source if f else None)


class ProjectFundAllocationRevisionSerializer(serializers.ModelSerializer):
    budget_item_code = serializers.CharField(source="allocation.budget_item.code", read_only=True, default=None)
    budget_item_name = serializers.CharField(source="allocation.budget_item.name", read_only=True, default=None)
    proposed_by_name = serializers.SerializerMethodField()
    decided_by_name = serializers.SerializerMethodField()
    proposed_budget_item_label = serializers.SerializerMethodField()

    class Meta:
        model = ProjectFundAllocationRevision
        fields = [
            "id", "fund", "allocation", "budget_item_code", "budget_item_name",
            "action", "status", "proposed_data", "previous_data",
            "proposed_budget_item_label", "reason",
            "proposed_by", "proposed_by_name", "proposed_at",
            "decided_by", "decided_by_name", "decided_at", "decision_note",
        ]
        read_only_fields = [
            "id", "status", "previous_data", "proposed_by", "proposed_by_name", "proposed_at",
            "decided_by", "decided_by_name", "decided_at", "decision_note",
            "budget_item_code", "budget_item_name", "proposed_budget_item_label",
        ]

    def get_proposed_by_name(self, obj):
        return obj.proposed_by.get_full_name() or obj.proposed_by.email if obj.proposed_by_id else None

    def get_decided_by_name(self, obj):
        return obj.decided_by.get_full_name() or obj.decided_by.email if obj.decided_by_id else None

    def get_proposed_budget_item_label(self, obj):
        item_id = obj.proposed_data.get("budget_item")
        if not item_id:
            return None
        # Batch-resolved by the ViewSet and passed via context to avoid an
        # N+1 query (proposed_data.budget_item is a raw UUID, not an FK).
        lookup = self.context.get("proposed_budget_items")
        if lookup is not None:
            item = lookup.get(str(item_id))
            return f"{item.code} {item.name}" if item else None
        from apps.finance.models import BudgetItem
        item = BudgetItem.objects.filter(id=item_id).first()
        return f"{item.code} {item.name}" if item else None


class FundingOpportunitySerializer(serializers.ModelSerializer):
    donor_name = serializers.SerializerMethodField()

    class Meta:
        model = FundingOpportunity
        fields = [
            "id", "fund", "name", "donor", "donor_name", "deadline",
            "ceiling", "stage", "notes", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def get_donor_name(self, obj):
        return obj.donor.name if obj.donor_id else None

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            from apps.core.tenancy import get_active_organization, assert_same_organization
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("fund"), attrs.get("donor"))
        return attrs


class FundingProposalSerializer(serializers.ModelSerializer):
    class Meta:
        model = FundingProposal
        fields = [
            "id", "fund", "title", "submitted_date", "requested_amount",
            "status", "document", "notes", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class SubmissionVersionSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    submitted_by_name = serializers.SerializerMethodField()

    class Meta:
        model = SubmissionVersion
        fields = ["id", "version_number", "file_url", "note", "submitted_by_name", "created_at"]
        read_only_fields = fields

    def get_file_url(self, obj):
        if not obj.file:
            return None
        request = self.context.get("request")
        url = obj.file.url
        return request.build_absolute_uri(url) if request else url

    def get_submitted_by_name(self, obj):
        if not obj.submitted_by_id:
            return None
        return obj.submitted_by.get_full_name() or obj.submitted_by.email


class FundReportSerializer(serializers.ModelSerializer):
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    kind_label = serializers.CharField(source="get_kind_display", read_only=True)

    class Meta:
        model = FundReport
        fields = [
            "id", "fund", "title", "kind", "kind_label", "period_start", "period_end",
            "due_date", "submitted_date", "status", "status_label", "document", "notes",
            "reviewed_at", "review_note", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "created_at", "updated_at", "status_label", "kind_label",
            "reviewed_at", "review_note",
        ]

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class DeliverableSerializer(serializers.ModelSerializer):
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    type_label = serializers.CharField(source="get_deliverable_type_display", read_only=True)
    project_name = serializers.CharField(source="fund.project.name", read_only=True, default=None)
    versions = SubmissionVersionSerializer(many=True, read_only=True, source="_versions")

    class Meta:
        model = Deliverable
        fields = [
            "id", "fund", "milestone", "title", "deliverable_type", "type_label",
            "description", "acceptance_criteria", "due_date", "submitted_date",
            "approved_date", "status", "status_label", "donor_visible", "project_name",
            "reviewed_at", "review_note", "versions", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "created_at", "updated_at", "status_label", "type_label", "project_name",
            "submitted_date", "approved_date", "reviewed_at", "review_note", "versions",
        ]

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)

    def to_representation(self, instance):
        if not hasattr(instance, "_versions"):
            from django.contrib.contenttypes.models import ContentType
            instance._versions = SubmissionVersion.objects.filter(
                content_type=ContentType.objects.get_for_model(Deliverable),
                object_id=str(instance.id),
            ).select_related("submitted_by")
        return super().to_representation(instance)


class IndicatorRecordSerializer(serializers.ModelSerializer):
    class Meta:
        model = IndicatorRecord
        fields = ["id", "indicator", "period_date", "value", "note", "created_at"]
        read_only_fields = ["id", "created_at"]

    def validate_indicator(self, value):
        # IndicatorRecord has no organization column of its own; ownership is
        # reached through indicator.project. Without this, a caller in org A can
        # POST an org-B indicator id and DRF resolves it against the model's
        # unrestricted default queryset (BE-002 class of bug).
        #
        # Raised by hand rather than via validate_same_organization_field so the
        # message names the indicator pk the caller actually sent — and stays
        # indistinguishable from a genuinely invalid pk, so it can't be used to
        # probe which ids exist in other organizations.
        from apps.core.tenancy import get_active_organization

        request = self.context.get("request")
        assert request is not None, (
            "IndicatorRecordSerializer must receive request in its context for "
            "cross-organization FK validation to run."
        )
        if value.project.organization_id != get_active_organization(request).id:
            raise serializers.ValidationError(
                f'Invalid pk "{value.pk}" - object does not exist.',
                code="does_not_exist",
            )
        return value


class ProjectIndicatorSerializer(serializers.ModelSerializer):
    level_label = serializers.CharField(source="get_result_level_display", read_only=True)
    current_value = serializers.SerializerMethodField()
    achievement_pct = serializers.SerializerMethodField()
    records = IndicatorRecordSerializer(many=True, read_only=True)

    class Meta:
        model = ProjectIndicator
        fields = [
            "id", "project", "code", "name", "description", "result_level", "level_label",
            "unit", "baseline", "target", "aggregation", "data_source", "frequency",
            "donor_visible", "current_value", "achievement_pct", "records",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at", "level_label", "current_value", "achievement_pct", "records"]

    def validate_project(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)

    def get_current_value(self, obj):
        value = obj.current_value()
        return f"{value:.2f}" if value is not None else None

    def get_achievement_pct(self, obj):
        value = obj.current_value()
        if value is None or not obj.target:
            return None
        return round(float(value) / float(obj.target) * 100, 1)


class ProjectRiskSerializer(serializers.ModelSerializer):
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    category_label = serializers.CharField(source="get_category_display", read_only=True)
    score = serializers.IntegerField(read_only=True)
    owner_name = serializers.SerializerMethodField()

    class Meta:
        model = ProjectRisk
        fields = [
            "id", "project", "kind", "title", "category", "category_label", "description",
            "probability", "impact", "score", "mitigation", "owner", "owner_name",
            "status", "status_label", "target_date", "resolved_date", "donor_visible",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at", "status_label", "category_label", "score", "owner_name"]

    def validate_project(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)

    def get_owner_name(self, obj):
        if not obj.owner_id:
            return None
        return obj.owner.get_full_name() or obj.owner.email


class ProjectFundTrancheSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProjectFundTranche
        fields = [
            "id", "fund", "label", "amount", "expected_date", "received_date",
            "status", "notes", "journal_entry", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "journal_entry", "created_at", "updated_at"]

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class ProjectFundComplianceSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProjectFundCompliance
        fields = [
            "id", "fund", "title", "kind", "due_date", "status",
            "notes", "created_at", "updated_at",
        ]
        read_only_fields = ["id", "created_at", "updated_at"]

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


class FundDocumentSerializer(serializers.ModelSerializer):
    category_label = serializers.CharField(source="get_category_display", read_only=True)
    document_url = serializers.SerializerMethodField()

    class Meta:
        model = FundDocument
        fields = [
            "id", "fund", "category", "category_label", "title",
            "document", "document_url", "date", "amount", "status", "notes",
            "created_at", "updated_at",
        ]
        read_only_fields = ["id", "category_label", "document_url", "created_at", "updated_at"]

    def get_document_url(self, obj):
        if not obj.document:
            return None
        request = self.context.get("request")
        url = obj.document.url
        return request.build_absolute_uri(url) if request else url

    def validate_fund(self, value):
        from apps.core.tenancy import validate_same_organization_field
        return validate_same_organization_field(self, value)


def _user_name(user):
    if not user:
        return None
    return user.get_full_name() or user.email


class PRFAttachmentSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    uploaded_by_name = serializers.SerializerMethodField()

    class Meta:
        model = PRFAttachment
        fields = ["id", "prf", "slug", "file", "file_url", "uploaded_by", "uploaded_by_name", "created_at"]
        read_only_fields = ["id", "file_url", "uploaded_by", "uploaded_by_name", "created_at"]
        extra_kwargs = {"file": {"write_only": True}}
        # The view upserts via update_or_create (re-uploading a slug replaces
        # the existing row), so the auto-generated unique-together validator
        # would wrongly reject that as a duplicate — validation here is for
        # the file itself, not this constraint.
        validators = []

    def validate_file(self, value):
        from apps.core.uploads import validate_upload
        validate_upload(value)
        return value

    def get_file_url(self, obj):
        if not obj.file:
            return None
        request = self.context.get("request")
        url = obj.file.url
        return request.build_absolute_uri(url) if request else url

    def get_uploaded_by_name(self, obj):
        return _user_name(obj.uploaded_by)


class PaymentRequestFormSerializer(serializers.ModelSerializer):
    """Payment Request Form (PRF). Reads are fully expanded (names, budget
    line labels); writes only touch the fields owned by whichever step is
    being performed — see PaymentRequestFormViewSet for the step actions."""
    status_label = serializers.CharField(source="get_status_display", read_only=True)
    fund_source = serializers.CharField(source="fund.source", read_only=True, default=None)
    fund_currency = serializers.CharField(source="fund.currency", read_only=True, default=None)
    budget_item_code = serializers.CharField(source="budget_item.code", read_only=True, default=None)
    budget_item_name = serializers.CharField(source="budget_item.name", read_only=True, default=None)
    vendor_name = serializers.CharField(source="vendor.name", read_only=True, default=None)
    document_url = serializers.SerializerMethodField()
    attachments = PRFAttachmentSerializer(many=True, read_only=True)

    created_by_name = serializers.SerializerMethodField()
    prepared_by_name = serializers.SerializerMethodField()
    reviewed_by_name = serializers.SerializerMethodField()
    budget_verified_by_name = serializers.SerializerMethodField()
    finance_approved_by_name = serializers.SerializerMethodField()

    class Meta:
        model = PaymentRequestForm
        fields = [
            "id", "project", "fund", "fund_source", "fund_currency", "budget_item",
            "budget_item_code", "budget_item_name", "number", "payment_date", "currency",
            "vendor", "vendor_name", "recipient_name", "company_name", "address", "npwp_nik",
            "bank_name", "account_number", "account_holder", "swift_code",
            "line_items", "amount", "notes", "attachments",
            "budget_available", "realized_before", "realized_after", "budget_remaining",
            "status", "status_label",
            "prepared_by", "prepared_by_name", "prepared_at",
            "reviewed_by", "reviewed_by_name", "reviewed_at", "review_comment",
            "budget_verified_by", "budget_verified_by_name", "budget_verified_at", "budget_verify_comment",
            "finance_approved_by", "finance_approved_by_name", "finance_approved_at", "approve_comment",
            "document", "document_url",
            "created_by", "created_by_name", "created_at", "updated_at",
        ]
        read_only_fields = [
            "id", "status", "status_label", "fund_source", "fund_currency",
            "budget_item_code", "budget_item_name", "vendor_name", "attachments",
            "budget_available", "realized_before", "realized_after", "budget_remaining",
            "prepared_by", "prepared_by_name", "prepared_at",
            "reviewed_by", "reviewed_by_name", "reviewed_at",
            "budget_verified_by", "budget_verified_by_name", "budget_verified_at",
            "finance_approved_by", "finance_approved_by_name", "finance_approved_at",
            "document", "document_url",
            "created_by", "created_by_name", "created_at", "updated_at",
        ]

    def get_document_url(self, obj):
        if not obj.document or not obj.document.file:
            return None
        request = self.context.get("request")
        url = obj.document.file.url
        return request.build_absolute_uri(url) if request else url

    def get_created_by_name(self, obj):
        return _user_name(obj.created_by)

    def get_prepared_by_name(self, obj):
        return _user_name(obj.prepared_by)

    def get_reviewed_by_name(self, obj):
        return _user_name(obj.reviewed_by)

    def get_budget_verified_by_name(self, obj):
        return _user_name(obj.budget_verified_by)

    def get_finance_approved_by_name(self, obj):
        return _user_name(obj.finance_approved_by)

    def validate(self, attrs):
        request = self.context.get("request")
        if request is not None:
            from apps.core.tenancy import get_active_organization, assert_same_organization
            org = get_active_organization(request)
            assert_same_organization(org, attrs.get("project"), attrs.get("fund"), attrs.get("vendor"))
            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."})
        return attrs
