"""Finance rules. Everything here is organization-level (the API requires organization scope).

Balance of an account = opening balance + income - expense. Transactions created from an invoice or
a payroll run are linked to their source (recorded at most once) and keep their amount; they can
only be deleted (then the source can be recorded again).
"""
from __future__ import annotations

import datetime

from django.db import transaction as db
from django.db.models import Q, Sum
from rest_framework.exceptions import NotFound, ValidationError

from apps.audit import services as audit
from apps.finance.models import FinanceAccount, FinanceCategory, Transaction
from domain.exceptions import DomainError
from domain.value_objects import jalali_month_to_gregorian_range

DEFAULT_CATEGORIES = [
    ("income", "فروش"), ("income", "سایر درآمدها"),
    ("expense", "حقوق و دستمزد"), ("expense", "اجاره"), ("expense", "قبوض و خدمات"),
    ("expense", "خرید و تدارکات"), ("expense", "سایر هزینه‌ها"),
]


class FinanceStateError(DomainError):
    pass


def ensure_default_categories(org) -> None:
    if not FinanceCategory.objects.filter(organization=org).exists():
        FinanceCategory.objects.bulk_create([FinanceCategory(organization=org, kind=k, name=n) for k, n in DEFAULT_CATEGORIES])


def _get(model, ctx, pk):
    obj = model.objects.filter(organization=ctx.organization, pk=pk).first()
    if obj is None:
        raise NotFound()
    return obj


# ---- accounts & categories
def with_balance(accounts):
    result = []
    for a in accounts:
        sums = {s["kind"]: s["t"] or 0 for s in a.transactions.values("kind").annotate(t=Sum("amount"))}
        result.append((a, a.opening_balance + sums.get("income", 0) - sums.get("expense", 0)))
    return result


def create_account(ctx, *, data: dict, request=None) -> FinanceAccount:
    name = str(data.get("name", "")).strip()
    if not name:
        raise ValidationError({"name": "Name is required."})
    if data.get("type", "bank") not in dict(FinanceAccount.TYPES):
        raise ValidationError({"type": "Invalid account type."})
    if FinanceAccount.objects.filter(organization=ctx.organization, name=name).exists():
        raise ValidationError({"name": "An account with this name already exists."})
    account = FinanceAccount.objects.create(
        organization=ctx.organization, name=name, type=data.get("type", "bank"), opening_balance=int(data.get("opening_balance", 0))
    )
    audit.record(actor=ctx.user, action="finance.account_create", organization=ctx.organization, entity_type="finance_account", entity_id=account.pk, request=request)
    return account


def update_account(ctx, pk: int, *, data: dict, request=None) -> FinanceAccount:
    account = _get(FinanceAccount, ctx, pk)
    if "name" in data:
        name = str(data["name"]).strip()
        if not name or FinanceAccount.objects.filter(organization=ctx.organization, name=name).exclude(pk=pk).exists():
            raise ValidationError({"name": "Invalid or duplicate name."})
        account.name = name
    if "is_active" in data:
        account.is_active = bool(data["is_active"])
    if "opening_balance" in data:
        account.opening_balance = int(data["opening_balance"])
    account.save()
    audit.record(actor=ctx.user, action="finance.account_update", organization=ctx.organization, entity_type="finance_account", entity_id=pk, request=request)
    return account


def create_category(ctx, *, data: dict, request=None) -> FinanceCategory:
    name, kind = str(data.get("name", "")).strip(), data.get("kind")
    if not name or kind not in dict(FinanceCategory.KINDS):
        raise ValidationError({"name": "A name and a kind (income/expense) are required."})
    if FinanceCategory.objects.filter(organization=ctx.organization, name=name, kind=kind).exists():
        raise ValidationError({"name": "This category already exists."})
    category = FinanceCategory.objects.create(organization=ctx.organization, name=name, kind=kind)
    audit.record(actor=ctx.user, action="finance.category_create", organization=ctx.organization, entity_type="finance_category", entity_id=category.pk, request=request)
    return category


# ---- transactions
def _check_transaction(ctx, data: dict, *, existing: Transaction | None = None) -> dict:
    values = {}
    kind = data.get("kind", existing.kind if existing else None)
    if kind not in dict(Transaction.KINDS):
        raise ValidationError({"kind": "Kind must be income or expense."})
    values["kind"] = kind
    if "amount" in data or existing is None:
        amount = int(data.get("amount", 0))
        if amount <= 0:
            raise ValidationError({"amount": "The amount must be positive."})
        values["amount"] = amount
    if "account" in data or existing is None:
        account = FinanceAccount.objects.filter(organization=ctx.organization, pk=data.get("account")).first()
        if account is None or not account.is_active:
            raise ValidationError({"account": "Unknown or inactive account."})
        values["account"] = account
    if "category" in data:
        if data["category"] is None:
            values["category"] = None
        else:
            category = FinanceCategory.objects.filter(organization=ctx.organization, pk=data["category"]).first()
            if category is None or category.kind != kind:
                raise ValidationError({"category": "The category does not exist or does not match the kind."})
            values["category"] = category
    for field in ("date", "description"):
        if field in data:
            values[field] = data[field]
    if existing is None and "date" not in values:
        raise ValidationError({"date": "The date is required."})
    return values


def create_transaction(ctx, *, data: dict, request=None) -> Transaction:
    values = _check_transaction(ctx, data)
    t = Transaction.objects.create(organization=ctx.organization, created_by=ctx.user, **values)
    audit.record(actor=ctx.user, action="finance.transaction_create", organization=ctx.organization, entity_type="finance_transaction", entity_id=t.pk, after={"kind": t.kind, "amount": t.amount}, request=request)
    return t


def update_transaction(ctx, pk: int, *, data: dict, request=None) -> Transaction:
    t = _get(Transaction, ctx, pk)
    if t.source_type != "manual" and ({"amount", "kind", "account"} & data.keys()):
        raise FinanceStateError("A transaction recorded from an invoice or payroll cannot change its amount, kind or account.")
    values = _check_transaction(ctx, data, existing=t)
    for key, value in values.items():
        setattr(t, key, value)
    t.save()
    audit.record(actor=ctx.user, action="finance.transaction_update", organization=ctx.organization, entity_type="finance_transaction", entity_id=t.pk, after={"kind": t.kind, "amount": t.amount}, request=request)
    return t


def delete_transaction(ctx, pk: int, *, request=None) -> None:
    t = _get(Transaction, ctx, pk)
    t.delete()
    audit.record(actor=ctx.user, action="finance.transaction_delete", organization=ctx.organization, entity_type="finance_transaction", entity_id=pk, request=request)


# ---- recording from other modules
def _auto_category(ctx, kind: str, name: str) -> FinanceCategory:
    return FinanceCategory.objects.get_or_create(organization=ctx.organization, kind=kind, name=name)[0]


def unrecorded(ctx) -> dict:
    """Paid invoices and approved payroll runs that are not in the books yet (only for enabled modules)."""
    result = {"invoices": [], "payroll_runs": []}
    recorded = lambda src: set(Transaction.objects.filter(organization=ctx.organization, source_type=src).values_list("source_id", flat=True))
    if "invoicing" in ctx.enabled_modules and ctx.has("invoicing.view", "organization"):
        from apps.invoicing.models import Invoice

        done = recorded("invoice")
        for inv in Invoice.objects.filter(organization=ctx.organization, status="paid").select_related("customer").order_by("number"):
            if inv.pk not in done:
                result["invoices"].append({"id": inv.pk, "number": inv.number, "customer": inv.customer.name, "total": inv.total, "date": (inv.paid_at or inv.created_at).date().isoformat()})
    if "payroll" in ctx.enabled_modules and ctx.has("payroll.view", "organization"):
        from apps.payroll.models import PayrollRun

        done = recorded("payroll")
        for run in PayrollRun.objects.filter(organization=ctx.organization, status="approved").prefetch_related("items"):
            if run.pk not in done:
                result["payroll_runs"].append({"id": run.pk, "year": run.year, "month": run.month, "total": sum(i.net for i in run.items.all()), "date": (run.approved_at or run.created_at).date().isoformat()})
    return result


@db.atomic
def record_source(ctx, *, source: str, source_id: int, account_id: int, request=None) -> Transaction:
    pending = unrecorded(ctx)
    key = "invoices" if source == "invoice" else "payroll_runs"
    item = next((x for x in pending[key] if x["id"] == source_id), None)
    if item is None:
        raise FinanceStateError("Nothing to record: it is not paid/approved, does not exist, or is already recorded.")
    account = FinanceAccount.objects.filter(organization=ctx.organization, pk=account_id, is_active=True).first()
    if account is None:
        raise ValidationError({"account": "Unknown or inactive account."})
    if source == "invoice":
        kind, category, desc = "income", _auto_category(ctx, "income", "فروش"), f"فاکتور شماره‌ی {item['number']} - {item['customer']}"
    else:
        kind, category, desc = "expense", _auto_category(ctx, "expense", "حقوق و دستمزد"), f"حقوق {item['year']}/{item['month']:02d}"
    t = Transaction.objects.create(
        organization=ctx.organization, account=account, category=category, kind=kind, amount=item["total"],
        date=datetime.date.fromisoformat(item["date"]), description=desc, source_type=source, source_id=source_id, created_by=ctx.user,
    )
    audit.record(actor=ctx.user, action="finance.record_source", organization=ctx.organization, entity_type="finance_transaction", entity_id=t.pk, after={"source": source, "source_id": source_id}, request=request)
    return t


# ---- reports
def summary(ctx, *, year: int | None, month: int | None) -> dict:
    qs = Transaction.objects.filter(organization=ctx.organization)
    if year and month:
        start, end = jalali_month_to_gregorian_range(year, month)
        qs = qs.filter(date__gte=start, date__lte=end)
    elif year:
        qs = qs.filter(date__gte=jalali_month_to_gregorian_range(year, 1)[0], date__lte=jalali_month_to_gregorian_range(year, 12)[1])
    totals = {r["kind"]: r["t"] or 0 for r in qs.values("kind").annotate(t=Sum("amount"))}
    by_category = [
        {"category": r["category__name"] or "بدون دسته", "kind": r["kind"], "total": r["t"]}
        for r in qs.values("category__name", "kind").annotate(t=Sum("amount")).order_by("-t")
    ]
    accounts = [{"id": a.pk, "name": a.name, "type": a.type, "is_active": a.is_active, "balance": b}
                for a, b in with_balance(FinanceAccount.objects.filter(organization=ctx.organization))]
    income, expense = totals.get("income", 0), totals.get("expense", 0)
    return {"income": income, "expense": expense, "net": income - expense, "by_category": by_category,
            "accounts": accounts, "balance_total": sum(a["balance"] for a in accounts if a["is_active"])}
