"""Persisted billing entities.

The pricing arithmetic lives in `pricing/` as pure functions over value
objects; nothing here reimplements it. These models hold what must survive a
process restart: what was invoiced, what corrections were accepted, what each
correction did, and what each statement run settled.

ops-contract: the finance export selects on `Account`, `Adjustment`,
`AdjustmentRecord`, `BillingEntry`, `Invoice`, `ReceivableEntry` and
`StatementRun` by class and by field, imported from `adjustments.models`. New
models and new fields alongside are fine; renaming or moving these is an outage.
"""

from __future__ import annotations

from django.db import models


class Account(models.Model):
    account_id = models.CharField(max_length=64, unique=True)
    settlement_policy = models.CharField(
        max_length=32,
        default="credit_forward",
        choices=[("credit_forward", "credit_forward"), ("restate", "restate")],
    )
    summaries_adopted_ordinal = models.IntegerField(null=True, blank=True)
    summary_adoption_id = models.CharField(max_length=64, null=True, blank=True)
    balance = models.FloatField(default=0)
    amount_due = models.FloatField(default=0)

    def __str__(self) -> str:
        return self.account_id


class Invoice(models.Model):
    invoice_id = models.CharField(max_length=64, unique=True)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="invoices")
    period_key = models.CharField(max_length=16)
    period_starts_on = models.CharField(max_length=16, default="")
    period_ends_on = models.CharField(max_length=16, default="")
    rule_set_key = models.CharField(max_length=64, default="")
    total = models.FloatField()
    issued_total = models.FloatField(null=True, blank=True)
    closed = models.BooleanField(default=False)
    issued = models.BooleanField(default=False)
    revision_of = models.CharField(max_length=64, null=True, blank=True)
    selected_exclusive_promotion = models.CharField(max_length=64, null=True, blank=True)
    non_selected_exclusive_promotions = models.JSONField(default=list, blank=True)
    # Immutable snapshot of what this invoice was priced from: the period, its
    # metered usage, and the rule set in force. Resolution replays corrections
    # against this, so it must never change once the invoice is issued.
    base_state = models.JSONField(default=dict, blank=True)


class LineItem(models.Model):
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name="line_items")
    usage_key = models.CharField(max_length=64)
    description = models.CharField(max_length=255)
    quantity = models.FloatField()
    base = models.FloatField()
    net = models.FloatField()
    tax = models.FloatField()
    total = models.FloatField()


class Adjustment(models.Model):
    """A correction delivered by a partner or an operator.

    The partner feed is at-least-once: the same `adjustment_id` may arrive
    repeatedly and must produce the same economics.
    """

    adjustment_id = models.CharField(max_length=64, unique=True)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="adjustments")
    period_key = models.CharField(max_length=16)
    kind = models.CharField(max_length=32)
    effective_at = models.CharField(max_length=32)
    received_at = models.CharField(max_length=32)
    acceptance_ordinal = models.IntegerField(null=True, blank=True)

    amount = models.FloatField(null=True, blank=True)
    usage_key = models.CharField(max_length=64, null=True, blank=True)
    quantity = models.FloatField(null=True, blank=True)
    promotion_key = models.CharField(max_length=64, null=True, blank=True)
    promotion_percent = models.FloatField(null=True, blank=True)
    promotion_kind = models.CharField(max_length=16, null=True, blank=True)
    promotion_applies_to = models.JSONField(default=list, blank=True)
    target_period_key = models.CharField(max_length=16, null=True, blank=True)

    subject_key = models.CharField(max_length=64, null=True, blank=True)
    replaces_adjustment_id = models.CharField(max_length=64, null=True, blank=True)
    withdraws_adjustment_id = models.CharField(max_length=64, null=True, blank=True)


class AdjustmentRecord(models.Model):
    """The marginal effect one adjustment had on one issued invoice."""

    record_id = models.CharField(max_length=96, unique=True)
    adjustment_id = models.CharField(max_length=64, db_index=True)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="records")
    acceptance_ordinal = models.IntegerField()
    original_invoice = models.ForeignKey(
        Invoice, on_delete=models.PROTECT, related_name="records"
    )
    target_period = models.CharField(max_length=16)
    delta = models.FloatField()
    prior_recorded_total = models.FloatField()
    resulting_total = models.FloatField()
    policy = models.CharField(max_length=32, default="")
    effective_at = models.CharField(max_length=32)
    applied_period = models.CharField(max_length=32, null=True, blank=True)
    replacement_invoice_id = models.CharField(max_length=64, null=True, blank=True)
    void_amount = models.FloatField(null=True, blank=True)
    reissue_amount = models.FloatField(null=True, blank=True)
    replaces_adjustment_id = models.CharField(max_length=64, null=True, blank=True)
    withdraws_adjustment_id = models.CharField(max_length=64, null=True, blank=True)


class StatementRun(models.Model):
    run_id = models.CharField(max_length=64, unique=True)
    account = models.ForeignKey(
        Account, on_delete=models.PROTECT, related_name="statement_runs"
    )
    run_number = models.IntegerField()
    operation_id = models.CharField(max_length=64)
    predecessor_run_id = models.CharField(max_length=64, null=True, blank=True)
    previous_cutoff_ordinal = models.IntegerField(default=0)
    cutoff_ordinal = models.IntegerField()
    status = models.CharField(max_length=16, default="started")
    recorded_demand = models.FloatField(null=True, blank=True)
    incremental_demand = models.FloatField(null=True, blank=True)


class RunMembership(models.Model):
    run = models.ForeignKey(
        StatementRun, on_delete=models.CASCADE, related_name="membership"
    )
    adjustment_id = models.CharField(max_length=64)
    acceptance_ordinal = models.IntegerField()
    original_invoice_id = models.CharField(max_length=64)
    record = models.ForeignKey(AdjustmentRecord, on_delete=models.PROTECT)

    class Meta:
        ordering = ["acceptance_ordinal"]


class RunSummaryEntry(models.Model):
    """Per-invoice rollup recorded on an in-scope issued run."""

    run = models.ForeignKey(
        StatementRun, on_delete=models.CASCADE, related_name="reconciliation_summary"
    )
    invoice_id = models.CharField(max_length=64)
    member_count = models.IntegerField()
    member_delta_total = models.FloatField()

    class Meta:
        unique_together = ("run", "invoice_id")
        ordering = ["invoice_id"]


class BillingEntry(models.Model):
    entry_id = models.CharField(max_length=96, unique=True)
    account = models.ForeignKey(
        Account, on_delete=models.PROTECT, related_name="billing_entries"
    )
    source_type = models.CharField(max_length=16)
    source_id = models.CharField(max_length=64)
    amount = models.FloatField()
    period_key = models.CharField(max_length=16)
    invoice_id = models.CharField(max_length=64)
    acceptance_ordinal = models.IntegerField(null=True, blank=True)


class ReceivableEntry(models.Model):
    account = models.ForeignKey(
        Account, on_delete=models.PROTECT, related_name="receivable_entries"
    )
    run_id = models.CharField(max_length=64, unique=True)
    predecessor_run_id = models.CharField(max_length=64, null=True, blank=True)
    prior_demand = models.FloatField()
    demand = models.FloatField()
    incremental_demand = models.FloatField()


class PromotionCatalogEntry(models.Model):
    """The campaign catalogue, imported from the 2024 marketing export.

    One row per (campaign, tier) pair. Reporting joins against it to put a
    human label on a promotion key; nothing on the intake or settlement path
    reads it.
    """

    entry_key = models.CharField(max_length=96, unique=True)
    campaign_key = models.CharField(max_length=64, db_index=True)
    tier_key = models.CharField(max_length=32)
    label = models.CharField(max_length=128, default="")
    display_order = models.IntegerField(default=0)
    retired = models.BooleanField(default=False)

    class Meta:
        ordering = ["entry_key"]


class IntakeCheckpoint(models.Model):
    """Where a delivery got to, so an interrupted intake can resume.

    Stages: received -> accepted -> resolved -> applied -> posted.
    """

    adjustment_id = models.CharField(max_length=64, db_index=True)
    account = models.ForeignKey(
        Account, on_delete=models.PROTECT, related_name="checkpoints"
    )
    stage = models.CharField(max_length=16)
    applied_periods = models.JSONField(default=list, blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ("account", "adjustment_id")
