"""services/vendors — the PlusROI Vendors write surface (phase-3 slices 1
+ 2a, docs/design/plusroi-forms.md §2).

A **vendor** is thin: a **party** (org OR person) that an expense pays.
There is NO vendor profile carrying commercial terms — the payables model
references a party directly (``expenses.vendor_party_id``). But a vendor
DOES need to be listable the moment it's created (before any expense) and
to hold this tenant's per-vendor DEFAULTS (category / paid-by). That is
the **``tenant_vendors``** marker (alembic 0008, ADR #022 — slice 1
deferred it):

  * **Create** matches an existing ACTIVE party by (kind, lower(name))
    first — the ``services/registry`` dedup pattern — and REUSES it
    rather than minting a duplicate; a miss creates the party through the
    1:1 subtype choke point (``create_org_party`` / ``create_person_party``),
    never ``Party(...)`` directly. Then it ensures a ``tenant_vendors``
    marker for THIS tenant (idempotent), stamping the optional defaults.
  * **List** = the tenant's ``tenant_vendors`` markers ∪ the distinct
    parties this tenant's expenses reference — so a just-created vendor
    shows immediately AND a sheet-imported vendor (marker-less, but
    expense-referenced) still shows. Each row carries its expense count +
    the marker's defaults.

The marker is deliberately NOT an engagement: an engagement is a
commercial edge in the entity graph that can grant a party tenant data
access; a payee we merely pay must never gain that (ADR #022).

Every mutation is owner-gated (``can_transfer``), refused on sheet-owned
books, tenant-scoped, and audited (``services.plusroi_gates``).
"""

from __future__ import annotations

import uuid

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models import Expense, Party, TenantVendor
from app.services.audit import write_audit
from app.services.entities.parties import create_org_party, create_person_party
from app.services.entities.resolve import _allocate_slug
from app.services.errors import ServiceError
from app.services.plusroi_gates import (
    require_owner,
    require_tenant,
    require_writable_books,
)

VENDOR_KINDS = ("org", "person")


# --------------------------------------------------------------- helpers


def _clean_text(raw) -> str | None:
    if raw is None:
        return None
    trimmed = str(raw).strip()
    return trimmed or None


def _require_name(raw) -> str:
    name = (raw or "").strip()
    if not name:
        raise ServiceError(422, "NAME_REQUIRED", "A vendor name is required.")
    return name


def _clean_kind(raw) -> str:
    kind = (raw or "org").strip().lower()
    if kind not in VENDOR_KINDS:
        raise ServiceError(
            422,
            "BAD_KIND",
            "A vendor must be an organization or a person.",
            f"Got {raw!r}; expected 'org' or 'person'.",
        )
    return kind


def _validate_party(session: Session, raw) -> uuid.UUID | None:
    """Resolve an optional party-id reference (the default paid-by party)."""
    if raw in (None, ""):
        return None
    try:
        party_id = uuid.UUID(str(raw))
    except ValueError as exc:
        raise ServiceError(
            422, "BAD_PARTY", "default_paid_by_party_id must be a UUID."
        ) from exc
    if session.scalar(select(Party.id).where(Party.id == party_id)) is None:
        raise ServiceError(
            404, "PARTY_NOT_FOUND", "Selected default paid-by party not found."
        )
    return party_id


def _active_party_by_name(session: Session, kind: str, name_lower: str) -> Party | None:
    """The oldest ACTIVE party of this kind whose lower(name) matches, or
    None — the registry dedup query, scoped by kind so an org 'Acme' and a
    person 'Acme' stay distinct."""
    return session.scalars(
        select(Party)
        .where(
            Party.kind == kind,
            Party.is_active.is_(True),
            func.lower(Party.name) == name_lower,
        )
        .order_by(Party.created_at)
        .limit(1)
    ).first()


def _vendor_dto(
    party: Party,
    expense_count: int,
    marker: TenantVendor | None,
    paid_by_name: str | None,
) -> dict:
    return {
        "party_id": str(party.id),
        "name": party.name,
        "kind": party.kind,
        "email": party.email,
        "phone": party.phone,
        "expense_count": expense_count,
        # per-tenant defaults (from the tenant_vendors marker; None when the
        # vendor is only expense-referenced, i.e. has no marker yet)
        "default_category": marker.default_category if marker is not None else None,
        "default_paid_by": (
            {"id": str(marker.default_paid_by_party_id), "name": paid_by_name}
            if marker is not None and marker.default_paid_by_party_id is not None
            else None
        ),
        "listed_via": "marker" if marker is not None else "expense",
    }


# --------------------------------------------------------------- reads


def list_vendors(session: Session, actor) -> dict:
    """The tenant's vendors: its ``tenant_vendors`` markers ∪ the distinct
    parties its expenses pay. A marker-listed vendor shows even with zero
    expenses (slice-1 gap closed) and carries this tenant's defaults; an
    expense-referenced vendor without a marker still shows. Owner-only,
    tenant-scoped."""
    require_owner(actor, "Only an owner can view vendors.")
    tenant_id = require_tenant(actor)

    # expense counts per referenced party (also the source of marker-less
    # sheet-imported vendors)
    counts = {
        pid: int(count)
        for pid, count in session.execute(
            select(Expense.vendor_party_id, func.count(Expense.id))
            .where(
                Expense.tenant_id == tenant_id,
                Expense.vendor_party_id.is_not(None),
            )
            .group_by(Expense.vendor_party_id)
        ).all()
    }

    # markers first — they carry the defaults + guarantee zero-expense
    # visibility.
    marker_rows = session.execute(
        select(TenantVendor, Party)
        .join(Party, Party.id == TenantVendor.vendor_party_id)
        .where(TenantVendor.tenant_id == tenant_id)
    ).all()

    # resolve default-paid-by names in one batch
    paid_by_ids = {
        tv.default_paid_by_party_id
        for tv, _ in marker_rows
        if tv.default_paid_by_party_id is not None
    }
    paid_by_names = (
        dict(
            session.execute(
                select(Party.id, Party.name).where(Party.id.in_(paid_by_ids))
            ).all()
        )
        if paid_by_ids
        else {}
    )

    vendors: dict[uuid.UUID, dict] = {}
    for tv, party in marker_rows:
        vendors[party.id] = _vendor_dto(
            party,
            counts.get(party.id, 0),
            tv,
            paid_by_names.get(tv.default_paid_by_party_id),
        )

    # expense-referenced parties WITHOUT a marker (e.g. sheet-imported)
    missing_ids = [pid for pid in counts if pid not in vendors]
    if missing_ids:
        for party in session.scalars(
            select(Party).where(Party.id.in_(missing_ids))
        ).all():
            vendors[party.id] = _vendor_dto(party, counts.get(party.id, 0), None, None)

    rows = sorted(vendors.values(), key=lambda v: v["name"].lower())
    return {"rows": rows}


# --------------------------------------------------------------- writes


def create_vendor(session: Session, actor, payload: dict) -> dict:
    """Ensure a payee party exists (org or person) AND a ``tenant_vendors``
    marker for this tenant (so it's immediately listable + holds the
    optional defaults). Match-existing-party-by-name first (reuse — no
    duplicate); else create through the subtype choke point. Idempotent on
    the marker (a re-create refreshes the defaults). Owner-only,
    writable-books, tenant-scoped, audited."""
    require_owner(actor, "Only an owner can create vendors.")
    require_writable_books(session, actor)
    tenant_id = require_tenant(actor)

    name = _require_name(payload.get("name"))
    kind = _clean_kind(payload.get("kind"))
    email = _clean_text(payload.get("email"))
    phone = _clean_text(payload.get("phone"))
    address = _clean_text(payload.get("address"))
    default_category = _clean_text(payload.get("default_category"))
    default_paid_by = _validate_party(session, payload.get("default_paid_by_party_id"))

    party = _active_party_by_name(session, kind, name.lower())
    reused = party is not None
    if party is None:
        make = create_org_party if kind == "org" else create_person_party
        party = make(
            session,
            name=name,
            slug=_allocate_slug(session, name),
            email=email,
            phone=phone,
            address=address,
        )

    # Ensure the tenant_vendors marker (idempotent). A re-create refreshes
    # the defaults only when they were provided (non-destructive on blanks).
    marker = session.get(TenantVendor, (tenant_id, party.id))
    marker_existed = marker is not None
    if marker is None:
        marker = TenantVendor(
            tenant_id=tenant_id,
            vendor_party_id=party.id,
            default_category=default_category,
            default_paid_by_party_id=default_paid_by,
        )
        session.add(marker)
    else:
        if default_category is not None:
            marker.default_category = default_category
        if default_paid_by is not None:
            marker.default_paid_by_party_id = default_paid_by
    session.flush()

    write_audit(
        session,
        actor,
        action="vendors.create",
        entity_type="vendor",
        entity_id=party.id,
        after={
            "reused_party": reused,
            "marker_existed": marker_existed,
            "kind": kind,
            "name": party.name,
            "default_category": marker.default_category,
            "default_paid_by_party_id": (
                str(marker.default_paid_by_party_id)
                if marker.default_paid_by_party_id is not None
                else None
            ),
        },
    )
    session.commit()

    paid_by_name = (
        session.scalar(
            select(Party.name).where(Party.id == marker.default_paid_by_party_id)
        )
        if marker.default_paid_by_party_id is not None
        else None
    )
    expense_count = int(
        session.scalar(
            select(func.count(Expense.id)).where(
                Expense.tenant_id == tenant_id,
                Expense.vendor_party_id == party.id,
            )
        )
        or 0
    )
    return {
        "ok": True,
        "reused_party": reused,
        "row": _vendor_dto(party, expense_count, marker, paid_by_name),
    }
