import secrets
import time

from django.db import models


def uuid7() -> str:
    """Generate a UUID v7 (time-ordered UUID), matching finance app convention."""
    nanoseconds = time.time_ns()
    uuid_int = (nanoseconds << 16) | secrets.randbits(48)
    return f"{uuid_int:032x}"


class Account(models.Model):
    """Chart of Accounts entry. The double-entry ledger posts against these.

    Mirrors the "Chart of Accounts" tab: code, name, type, normal balance.
    """
    TYPE_CHOICES = [
        ("Asset", "Asset"),
        ("Liability", "Liability"),
        ("Equity", "Equity"),
        ("Income", "Income"),
        ("Expense", "Expense"),
        ("Encumbrance", "Encumbrance"),
    ]
    BALANCE_CHOICES = [("Debit", "Debit"), ("Credit", "Credit")]
    CASH_FLOW_CHOICES = [
        ("operating", "Operasi"),
        ("investing", "Investasi"),
        ("financing", "Pendanaan"),
        ("none", "Bukan Arus Kas"),
    ]
    REVENUE_TYPE_CHOICES = [
        ("contribution", "Contribution"),
        ("exchange", "Exchange"),
    ]
    EXPENSE_CLASS_CHOICES = [
        ("direct", "Beban Langsung"),
        ("indirect", "Beban Tidak Langsung"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="accounts")
    code = models.CharField(max_length=20, help_text="e.g. 1000, 4000.")
    name = models.CharField(max_length=200)
    type = models.CharField(max_length=20, choices=TYPE_CHOICES, db_index=True)
    normal_balance = models.CharField(max_length=10, choices=BALANCE_CHOICES)
    is_cash = models.BooleanField(
        default=False, help_text="True for cash/bank accounts (the cash basis of the cash-flow statement).",
    )
    cash_flow_category = models.CharField(
        max_length=12, choices=CASH_FLOW_CHOICES, default="operating",
        help_text="ISAK 335 cash-flow section this account's movements belong to.",
    )
    revenue_type = models.CharField(
        max_length=12, choices=REVENUE_TYPE_CHOICES, blank=True,
        help_text="PSAK 35 revenue class. Only meaningful for Income-type accounts.",
    )
    parent = models.ForeignKey(
        "self", on_delete=models.SET_NULL, null=True, blank=True, related_name="children",
        help_text=(
            "Grouping header this account rolls up to on the Penerimaan dan Pengeluaran "
            "report (e.g. 5100 Travel under 2000 Operasional Riset). Header accounts "
            "carry no postings themselves."
        ),
    )
    expense_class = models.CharField(
        max_length=10, choices=EXPENSE_CLASS_CHOICES, blank=True,
        help_text=(
            "Splits Beban Langsung from Beban Tidak Langsung (overhead) on the "
            "Penerimaan dan Pengeluaran report. Only meaningful for Expense-type accounts; "
            "blank is treated as direct."
        ),
    )
    notes = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_accounts"
        ordering = ["code"]
        verbose_name = "account"
        verbose_name_plural = "chart of accounts"
        constraints = [models.UniqueConstraint(fields=["organization", "code"], name="unique_account_code_per_org")]

    def __str__(self):
        return f"{self.code} {self.name}"


class Fund(models.Model):
    """A donor fund / grant. Mirrors the "Funds & Grants" tab.

    Award and Received are stored; Spent and Available are computed from posted
    journal lines (see views), so they are not persisted here.
    """
    FUND_TYPE_CHOICES = [
        ("Restricted", "Restricted"),
        ("Unrestricted", "Unrestricted"),
        ("Designated", "Designated"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="accounting_funds_scoped")
    fund_id = models.CharField(max_length=30, help_text="Business key, e.g. FND-001.")
    donor = models.ForeignKey(
        "companies.Company", on_delete=models.PROTECT, null=True, blank=True,
        related_name="accounting_funds", help_text="Donor company (optional).",
    )
    donor_name = models.CharField(max_length=200, blank=True, help_text="Free-text donor when not a Company.")
    name = models.CharField(max_length=255, help_text="Grant / fund name.")
    fund_type = models.CharField(max_length=20, choices=FUND_TYPE_CHOICES, default="Restricted")
    project = models.ForeignKey(
        "projects.Project", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_funds",
    )
    project_fund = models.ForeignKey(
        "projects.ProjectFund", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_funds",
        help_text="Links this ledger fund to the originating projects.ProjectFund grant record.",
    )
    project_code = models.CharField(max_length=50, blank=True, help_text="Free-text project tag, e.g. PRJ-HEALTH.")
    start_date = models.DateField(null=True, blank=True)
    end_date = models.DateField(null=True, blank=True)
    award_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, help_text="Total awarded (IDR).")
    received_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, help_text="Received to date (IDR).")
    currency = models.CharField(max_length=10, default="IDR")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_funds"
        ordering = ["fund_id"]
        constraints = [models.UniqueConstraint(fields=["organization", "fund_id"], name="unique_fund_id_per_org")]

    def __str__(self):
        return f"{self.fund_id} {self.name}"


class Transaction(models.Model):
    """Operational transaction register. Mirrors the "Transactions" tab.

    A higher-level operational record (one per business event). Net amounts in
    IDR are computed from original amount x exchange rate on save.
    """
    TYPE_CHOICES = [
        ("Grant Receipt", "Grant Receipt"),
        ("Expense", "Expense"),
        ("Advance", "Advance"),
        ("Advance Settlement", "Advance Settlement"),
        ("Allocation", "Allocation"),
    ]
    STATUS_CHOICES = [
        ("Draft", "Draft"),
        ("Pending Approval", "Pending Approval"),
        ("Posted", "Posted"),
        ("Rejected", "Rejected"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="transactions_scoped")
    transaction_id = models.CharField(max_length=30, help_text="Business key, e.g. TRX-001.")
    date = models.DateField()
    description = models.CharField(max_length=255, blank=True)
    transaction_type = models.CharField(max_length=30, choices=TYPE_CHOICES, db_index=True)
    fund = models.ForeignKey(Fund, on_delete=models.SET_NULL, null=True, blank=True, related_name="transactions")
    donor_name = models.CharField(max_length=200, blank=True)
    project_code = models.CharField(max_length=50, blank=True)
    activity = models.CharField(max_length=200, blank=True)
    budget_line = models.CharField(max_length=100, blank=True)
    # Optional links to the originating budget catalog line and source documents.
    # All nullable; the free-text fields above remain as a fallback when no
    # structured record exists.
    budget_item = models.ForeignKey(
        "finance.BudgetItem", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_transactions",
    )
    source_receivable_invoice = models.ForeignKey(
        "finance.Invoice", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_transactions", help_text="Donor (receivable) invoice this records.",
    )
    source_payable_invoice = models.ForeignKey(
        "procurements.Invoice", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_transactions", help_text="Vendor (payable) invoice this records.",
    )
    source_payment = models.ForeignKey(
        "procurements.Payment", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_transactions", help_text="Vendor payment this records.",
    )
    source_project_fund = models.ForeignKey(
        "projects.ProjectFund", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_transactions", help_text="Originating project grant/fund record.",
    )
    account = models.ForeignKey(Account, on_delete=models.SET_NULL, null=True, blank=True, related_name="transactions")
    counterparty = models.CharField(max_length=200, blank=True)
    currency = models.CharField(max_length=10, default="IDR")
    original_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    exchange_rate = models.DecimalField(max_digits=14, decimal_places=4, default=1)
    debit_idr = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    credit_idr = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    net_expense_idr = models.DecimalField(max_digits=18, decimal_places=2, default=0, help_text="Computed.")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="Draft", db_index=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_transactions"
        ordering = ["-date", "-created_at"]
        indexes = [
            models.Index(fields=["transaction_type"]),
            models.Index(fields=["status"]),
            models.Index(fields=["fund"]),
        ]
        constraints = [models.UniqueConstraint(fields=["organization", "transaction_id"], name="unique_transaction_id_per_org")]

    def __str__(self):
        return f"{self.transaction_id} {self.description}".strip()


class JournalEntry(models.Model):
    """A double-entry journal voucher. Mirrors a "Journal No." group.

    Holds the shared header; debit/credit detail lives in JournalLine. A journal
    is balanced when the sum of its line debits equals the sum of its credits.
    The General Ledger and Trial Balance are computed from posted journals.
    """
    STATUS_CHOICES = [
        ("Draft", "Draft"),
        ("Posted", "Posted"),
        ("Rejected", "Rejected"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="journal_entries_scoped")
    journal_no = models.CharField(max_length=30, help_text="e.g. JV-001.")
    date = models.DateField()
    reference = models.CharField(max_length=50, blank=True, help_text="Source ref, e.g. TRX-001.")
    description = models.CharField(max_length=255, blank=True)
    fund = models.ForeignKey(Fund, on_delete=models.SET_NULL, null=True, blank=True, related_name="journal_entries")
    donor_name = models.CharField(max_length=200, blank=True)
    project_code = models.CharField(max_length=50, blank=True)
    activity = models.CharField(max_length=200, blank=True)
    budget_line = models.CharField(max_length=100, blank=True)
    # Optional links to the budget catalog line and the source document this
    # voucher records. All nullable; free-text fields above remain a fallback.
    budget_item = models.ForeignKey(
        "finance.BudgetItem", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_journal_entries",
    )
    source_transaction = models.ForeignKey(
        Transaction, on_delete=models.SET_NULL, null=True, blank=True,
        related_name="journal_entries", help_text="The operational transaction this voucher posts.",
    )
    source_receivable_invoice = models.ForeignKey(
        "finance.Invoice", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_journal_entries", help_text="Donor (receivable) invoice this records.",
    )
    source_payable_invoice = models.ForeignKey(
        "procurements.Invoice", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_journal_entries", help_text="Vendor (payable) invoice this records.",
    )
    source_payment = models.ForeignKey(
        "procurements.Payment", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_journal_entries", help_text="Vendor payment this records.",
    )
    source_project_fund = models.ForeignKey(
        "projects.ProjectFund", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="accounting_journal_entries", help_text="Originating project grant/fund record.",
    )
    source_sale = models.ForeignKey(
        "Sale", on_delete=models.SET_NULL, null=True, blank=True,
        related_name="posting_journal_entries", help_text="Book/accessory sale this voucher records.",
    )
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="Draft", db_index=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_journal_entries"
        ordering = ["journal_no"]
        indexes = [models.Index(fields=["status"]), models.Index(fields=["date"])]
        verbose_name_plural = "journal entries"
        constraints = [models.UniqueConstraint(fields=["organization", "journal_no"], name="unique_journal_no_per_org")]

    def totals(self):
        """Return (total_debit, total_credit) over this journal's lines."""
        from decimal import Decimal
        debit = sum((l.debit_idr for l in self.lines.all()), Decimal("0"))
        credit = sum((l.credit_idr for l in self.lines.all()), Decimal("0"))
        return debit, credit

    def __str__(self):
        return self.journal_no


class JournalLine(models.Model):
    """One debit/credit line of a JournalEntry, posted against an Account."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    journal = models.ForeignKey(JournalEntry, on_delete=models.CASCADE, related_name="lines")
    account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="journal_lines")
    description = models.CharField(max_length=255, blank=True)
    debit_idr = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    credit_idr = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "accounting_journal_lines"
        ordering = ["created_at"]
        indexes = [models.Index(fields=["journal"]), models.Index(fields=["account"])]

    def __str__(self):
        return f"{self.journal_id} {self.account_id} D{self.debit_idr}/C{self.credit_idr}"


class SalesAccountingSettings(models.Model):
    """One row per organization: which accounts book/accessory sales post
    against. Commercial book/merch sales are a single activity per org, not
    tracked per-title."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.OneToOneField("companies.Organization", on_delete=models.CASCADE, related_name="sales_accounting_settings")
    cash_account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+", help_text="Debited with the sale total (cash or AR).")
    revenue_account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+", help_text="Credited with the sale total (Exchange Revenue income account).")
    cogs_account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+", help_text="Debited with cost of goods sold.")
    inventory_account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+", help_text="Credited with cost of goods sold (inventory asset account).")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_sales_settings"
        verbose_name_plural = "sales accounting settings"

    def __str__(self):
        return "Sales Accounting Settings"

    @classmethod
    def get_solo(cls, organization):
        return cls.objects.filter(organization=organization).first()


class Sale(models.Model):
    """A sale of a book or accessory to a buyer. Mirrors a point-of-sale receipt.

    Posting creates a stock-out StockMovement and a balanced JournalEntry
    recognizing Exchange Revenue and COGS via SalesAccountingSettings.
    """
    STATUS_CHOICES = [("Draft", "Draft"), ("Posted", "Posted"), ("Void", "Void")]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="sales_scoped")
    sale_no = models.CharField(max_length=30, help_text="e.g. SALE-001.")
    date = models.DateField()
    item = models.ForeignKey(
        "asset_management.SellableItem", on_delete=models.PROTECT, related_name="sales",
    )
    quantity = models.PositiveIntegerField()
    unit_price = models.DecimalField(max_digits=14, decimal_places=2)
    total_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0, help_text="quantity x unit_price. Computed.")
    buyer_name = models.CharField(max_length=200, blank=True)
    buyer_contact = models.ForeignKey(
        "contacts.Contact", on_delete=models.SET_NULL, null=True, blank=True, related_name="purchases",
    )
    stock_movement = models.ForeignKey(
        "asset_management.StockMovement", on_delete=models.SET_NULL, null=True, blank=True, related_name="sale",
    )
    journal_entry = models.ForeignKey(
        JournalEntry, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale",
    )
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="Draft", db_index=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "accounting_sales"
        ordering = ["-date", "-created_at"]
        indexes = [models.Index(fields=["status"])]
        constraints = [models.UniqueConstraint(fields=["organization", "sale_no"], name="unique_sale_no_per_org")]

    def recompute_total(self):
        from decimal import Decimal
        self.total_amount = (self.quantity or 0) * (self.unit_price or Decimal("0"))

    def __str__(self):
        return self.sale_no
