"""Invoicing rules.

draft -> issued (gets the next number, locked) -> paid, or issued -> cancelled. Only drafts can be
edited or deleted. Money is whole rials: line = quantity x unit price, subtotal = sum of lines,
tax = (subtotal - discount) x tax% , total = subtotal - discount + tax.

Scope of invoicing.view / update / delete: `self` = invoices I created or for customers I own;
`organization` (or wider) = every invoice of the organization.
"""
from __future__ import annotations

from decimal import ROUND_HALF_UP, Decimal

from django.db import transaction
from django.db.models import Max, Q
from django.utils import timezone
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError

from apps.audit import services as audit
from apps.crm.services import visible_customers
from apps.invoicing.models import Invoice, InvoiceItem
from domain.access_policy import scope_rank
from domain.exceptions import DomainError


class InvoiceStateError(DomainError):
    pass


def _money(value: Decimal) -> int:
    return int(value.quantize(Decimal("1"), rounding=ROUND_HALF_UP))


def visible_invoices(ctx, permission: str):
    qs = Invoice.objects.filter(organization=ctx.organization).select_related("customer", "created_by").prefetch_related("items")
    scope = ctx.effective.scope_of(permission)
    if scope is not None and scope_rank(scope) >= scope_rank("organization"):
        return qs
    return qs.filter(Q(created_by=ctx.user) | Q(customer__owner=ctx.user))


def get_invoice(ctx, pk: int, permission: str) -> Invoice:
    invoice = visible_invoices(ctx, permission).filter(pk=pk).first()
    if invoice is None:
        raise NotFound()
    return invoice


def usable_customers(ctx):
    """Customers the caller may bill (same visibility rule as the CRM)."""
    return visible_customers(ctx, "crm.view").filter(status__in=["lead", "active"])


def _customer(ctx, customer_id):
    customer = visible_customers(ctx, "crm.view").filter(pk=customer_id).first()
    if customer is None:
        raise ValidationError({"customer": "Unknown customer."})
    return customer


def _clean_items(items) -> list[dict]:
    if not items:
        raise ValidationError({"items": "An invoice needs at least one line."})
    cleaned = []
    for raw in items:
        description = str(raw.get("description", "")).strip()
        quantity = Decimal(str(raw.get("quantity", 0)))
        price = int(raw.get("unit_price", 0))
        if not description:
            raise ValidationError({"items": "Every line needs a description."})
        if quantity <= 0 or price < 0:
            raise ValidationError({"items": "Quantity must be positive and the price cannot be negative."})
        cleaned.append({"description": description, "quantity": quantity, "unit_price": price, "line_total": _money(quantity * price)})
    return cleaned


def _totals(items: list[dict], discount: int, tax_percent: Decimal) -> dict:
    subtotal = sum(i["line_total"] for i in items)
    if discount < 0 or discount > subtotal:
        raise ValidationError({"discount": "The discount must be between zero and the subtotal."})
    if not Decimal(0) <= tax_percent <= Decimal(100):
        raise ValidationError({"tax_percent": "Tax must be between 0 and 100."})
    tax = _money(Decimal(subtotal - discount) * tax_percent / 100)
    return {"subtotal": subtotal, "tax": tax, "total": subtotal - discount + tax}


def _write(invoice: Invoice, items: list[dict]) -> None:
    invoice.items.all().delete()
    InvoiceItem.objects.bulk_create([InvoiceItem(invoice=invoice, **i) for i in items])


@transaction.atomic
def create_invoice(ctx, *, data: dict, request=None) -> Invoice:
    customer = _customer(ctx, data.get("customer"))
    items = _clean_items(data.get("items"))
    tax_percent = Decimal(str(data.get("tax_percent", 10)))
    discount = int(data.get("discount", 0))
    invoice = Invoice.objects.create(
        organization=ctx.organization, customer=customer, created_by=ctx.user,
        issue_date=data["issue_date"], due_date=data.get("due_date"), notes=data.get("notes", ""),
        tax_percent=tax_percent, discount=discount, **_totals(items, discount, tax_percent),
    )
    _write(invoice, items)
    audit.record(actor=ctx.user, action="invoice.create", organization=ctx.organization, entity_type="invoice", entity_id=invoice.pk, request=request)
    return invoice


@transaction.atomic
def update_invoice(ctx, invoice: Invoice, *, data: dict, request=None) -> Invoice:
    if invoice.status != "draft":
        raise InvoiceStateError("Only a draft invoice can be edited.")
    if "customer" in data:
        invoice.customer = _customer(ctx, data["customer"])
    for field in ("issue_date", "due_date", "notes"):
        if field in data:
            setattr(invoice, field, data[field])
    if "tax_percent" in data:
        invoice.tax_percent = Decimal(str(data["tax_percent"]))
    if "discount" in data:
        invoice.discount = int(data["discount"])
    items = _clean_items(data["items"]) if "items" in data else [
        {"line_total": i.line_total} for i in invoice.items.all()
    ]
    for key, value in _totals(items, invoice.discount, invoice.tax_percent).items():
        setattr(invoice, key, value)
    invoice.save()
    if "items" in data:
        _write(invoice, items)
    audit.record(actor=ctx.user, action="invoice.update", organization=ctx.organization, entity_type="invoice", entity_id=invoice.pk, request=request)
    return invoice


def delete_invoice(ctx, invoice: Invoice, *, request=None) -> None:
    if invoice.status != "draft":
        raise InvoiceStateError("Only a draft invoice can be deleted; cancel an issued one instead.")
    pk = invoice.pk
    invoice.delete()
    audit.record(actor=ctx.user, action="invoice.delete", organization=ctx.organization, entity_type="invoice", entity_id=pk, request=request)


@transaction.atomic
def issue_invoice(ctx, invoice: Invoice, *, request=None) -> Invoice:
    if invoice.status != "draft":
        raise InvoiceStateError("The invoice is already issued.")
    if not invoice.items.exists():
        raise InvoiceStateError("An invoice without lines cannot be issued.")
    last = Invoice.objects.select_for_update().filter(organization=ctx.organization).aggregate(m=Max("number"))["m"] or 0
    invoice.number = last + 1
    invoice.status = "issued"
    invoice.issued_at = timezone.now()
    invoice.save()
    audit.record(actor=ctx.user, action="invoice.issue", organization=ctx.organization, entity_type="invoice", entity_id=invoice.pk, after={"number": invoice.number}, request=request)
    return invoice


def mark_paid(ctx, invoice: Invoice, *, request=None) -> Invoice:
    if invoice.status != "issued":
        raise InvoiceStateError("Only an issued invoice can be marked as paid.")
    invoice.status, invoice.paid_at = "paid", timezone.now()
    invoice.save()
    audit.record(actor=ctx.user, action="invoice.pay", organization=ctx.organization, entity_type="invoice", entity_id=invoice.pk, request=request)
    return invoice


def cancel_invoice(ctx, invoice: Invoice, *, request=None) -> Invoice:
    if invoice.status != "issued":
        raise InvoiceStateError("Only an issued, unpaid invoice can be cancelled.")
    invoice.status, invoice.cancelled_at = "cancelled", timezone.now()
    invoice.save()
    audit.record(actor=ctx.user, action="invoice.cancel", organization=ctx.organization, entity_type="invoice", entity_id=invoice.pk, request=request)
    return invoice


def summary(ctx) -> dict:
    qs = visible_invoices(ctx, "invoicing.view")
    result = {}
    for status, _ in Invoice.STATUSES:
        rows = qs.filter(status=status)
        result[status] = {"count": rows.count(), "total": sum(r.total for r in rows)}
    return result
