"""Adapter base — the mapping machinery every vendor table plugs into.

NEOedge is an additional module on the customer's existing system: SAP, Microsoft Dynamics,
Oracle, or anything else that can hand over a document. Each of those names the same fact
differently — `BUKRS`, `DataAreaId` and `ORG_ID` are all "which company code posted this" —
and every one of them is wrong for the other two. So the vendor vocabulary lives HERE, in a
table, and nothing above this package ever sees it.

That containment is what makes the record comparable. Two edges on two different ERPs must
produce byte-identical records for the same document, or the whole design fails at the first
disagreement; the way to get there is one canonical grammar with a thin, declarative mapping
per vendor, not a canonicaliser per vendor.

Adding an ERP is a table, not a code path. If a new system needs logic rather than a table,
that is a signal the grammar is missing a concept — fix the grammar, not the adapter.
"""

from __future__ import annotations

from dataclasses import dataclass, field

from ..grammar import DOC_KINDS, GrammarError, REFUSED_BAD_FIELD, REFUSED_MISSING_FIELD
from ..money import parse_native


@dataclass(frozen=True)
class FieldMap:
    """One vendor's vocabulary. Every entry is a priority list: the first name present wins,
    because real systems carry the same fact under different names per document type."""

    system_id: str
    company_code: tuple[str, ...]
    document_no: tuple[str, ...]
    fiscal_year: tuple[str, ...]
    doc_date: tuple[str, ...]
    amount: tuple[str, ...]
    currency: tuple[str, ...]
    doc_type: tuple[str, ...]
    lines: tuple[str, ...] = ()
    line_sku: tuple[str, ...] = ()
    line_quantity: tuple[str, ...] = ()
    line_unit: tuple[str, ...] = ()
    line_unit_price: tuple[str, ...] = ()
    # native document-type code → canonical doc_kind
    doc_kinds: dict[str, str] = field(default_factory=dict)
    default_amount_basis: str = "GROSS"


class AdapterError(GrammarError):
    """The ERP handed over something the grammar cannot represent. Named, never guessed at."""


class ErpAdapter:
    """Turns one native ERP document into the ERP-derived half of a record.

    It deliberately does NOT produce a whole record: `member_id`, `role`, `counterparty_id`
    and `relation_ref` are the edge's business, not the ERP's. An adapter that could fill
    those in would be able to assert who it is trading with on the strength of a field in a
    document — the Arena has already paid for that class of mistake once.
    """

    def __init__(self, field_map: FieldMap):
        self.map = field_map

    @property
    def system_id(self) -> str:
        return self.map.system_id

    # ── reading ──────────────────────────────────────────────────────────────────────

    def _first(self, native: dict, names: tuple[str, ...]) -> tuple[str | None, object]:
        for name in names:
            if name in native and native[name] not in (None, ""):
                return name, native[name]
        return None, None

    def _require(self, native: dict, names: tuple[str, ...], what: str):
        name, value = self._first(native, names)
        if name is None:
            raise AdapterError(
                REFUSED_MISSING_FIELD,
                f"{self.system_id}: no {what} — looked for {list(names)}, document has {sorted(native)}",
            )
        return value

    def doc_kind(self, native: dict) -> str:
        """The canonical kind, from the vendor's own type code."""
        raw = self._require(native, self.map.doc_type, "document type")
        kind = self.map.doc_kinds.get(str(raw))
        if kind is None:
            raise AdapterError(
                REFUSED_BAD_FIELD,
                f"{self.system_id}: document type {raw!r} maps to no canonical kind — "
                f"known: {sorted(set(self.map.doc_kinds.values()))}",
            )
        if kind not in DOC_KINDS:
            raise AdapterError(REFUSED_BAD_FIELD, f"{self.system_id}: {kind!r} is not a canonical doc kind")
        return kind

    def to_draft(self, native: dict, *, amount_basis: str | None = None) -> dict:
        """The ERP-derived fields, canonicalised. The edge completes the record."""
        kind = self.doc_kind(native)
        document_no = str(self._require(native, self.map.document_no, "document number"))
        doc_date = _as_date(self._require(native, self.map.doc_date, "document date"), self.system_id)

        _, fiscal_year = self._first(native, self.map.fiscal_year)
        fiscal_year = str(fiscal_year) if fiscal_year else doc_date[:4]

        draft = {
            "doc_kind": kind,
            "own_doc_ref": {
                "system": self.system_id,
                "company_code": str(self._require(native, self.map.company_code, "company code")).upper(),
                "document_no": document_no,
                "fiscal_year": fiscal_year,
            },
            "originator_doc_no": document_no,
            "doc_date": doc_date,
        }

        _, currency = self._first(native, self.map.currency)
        _, amount = self._first(native, self.map.amount)
        if currency and amount is not None:
            draft["currency"] = str(currency).upper()
            draft["amount"] = parse_native(amount, draft["currency"])
            draft["amount_basis"] = amount_basis or self.map.default_amount_basis

        lines = self._lines(native, draft.get("currency"))
        if lines:
            draft["lines"] = lines
        return draft

    def _lines(self, native: dict, currency: str | None) -> list[dict]:
        _, raw = self._first(native, self.map.lines)
        if not raw:
            return []
        if not isinstance(raw, list):
            raise AdapterError(REFUSED_BAD_FIELD, f"{self.system_id}: line items must be a list")
        out = []
        for i, native_line in enumerate(raw):
            if not isinstance(native_line, dict):
                raise AdapterError(REFUSED_BAD_FIELD, f"{self.system_id}: line {i} is not an object")
            line = {
                "sku": str(self._require(native_line, self.map.line_sku, f"line {i} sku")),
                "quantity": _as_decimal_string(
                    self._require(native_line, self.map.line_quantity, f"line {i} quantity"), self.system_id
                ),
                "unit": str(self._require(native_line, self.map.line_unit, f"line {i} unit")),
            }
            _, price = self._first(native_line, self.map.line_unit_price)
            if price is not None and currency:
                line["unit_price"] = parse_native(price, currency)
            out.append(line)
        return out


def _as_date(value, system_id: str) -> str:
    """ERPs write dates every way there is. Only unambiguous forms are accepted — a guess
    here would put `03/04` into the record as either March or April, and the counterparty's
    edge would guess the other way."""
    text = str(value).strip()
    if len(text) == 10 and text[4] == "-" and text[7] == "-":
        return text                                   # 2026-07-26
    if len(text) == 8 and text.isdigit():
        return f"{text[:4]}-{text[4:6]}-{text[6:]}"    # 20260726 (SAP internal)
    raise AdapterError(
        REFUSED_BAD_FIELD,
        f"{system_id}: {text!r} is not an unambiguous date — use YYYY-MM-DD or YYYYMMDD; "
        "a locale-dependent date would be read differently by the two edges",
    )


def _as_decimal_string(value, system_id: str) -> str:
    if isinstance(value, float):
        raise AdapterError(
            REFUSED_BAD_FIELD,
            f"{system_id}: quantity arrived as a float — read it as a string or an integer",
        )
    if isinstance(value, int):
        return str(value)
    text = str(value).strip()
    if "," in text:
        raise AdapterError(
            REFUSED_BAD_FIELD,
            f"{system_id}: quantity {text!r} uses a comma — the edge does not guess whether "
            "that is a decimal point or a thousands separator",
        )
    return text
