"""Write-Workflow: schreibt Felder zurück, Re-Read, Tag, Audit-Stempel.

Pflicht-Pfad ist Re-Read über ``fetch_one_document_meta_via_list`` (NICHT
über ``getOneDocument``), weil getOneDocument gegen das Per-UID-Daily-Limit
zählt.

``silently_dropped``-Erkennung mit Type-Normalisierung (Strings vs.
Floats): die API antwortet bei getDocumentList oft mit Strings, während
die Sender-Seite Floats schickt. Ohne Normalisierung wären falsche
Drop-Meldungen die Regel.

Audit-Stempel-Format (Bundle-Format-Version v1, eingefroren ab v0.10.0):

    [Agent v1, YYYY-MM-DD HH:MM] Ergaenzt: <feldliste>. Critic: <status>.

Bei nicht-leerem ``silently_dropped`` wird der Tag automatisch auf
``agent-review`` umgestellt.
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional


_SKILL_DIR = Path(__file__).resolve().parent.parent
_SKILLS_ROOT = _SKILL_DIR.parent
if str(_SKILLS_ROOT) not in sys.path:
    sys.path.insert(0, str(_SKILLS_ROOT))
if str(_SKILL_DIR) not in sys.path:
    sys.path.insert(0, str(_SKILL_DIR))


AGENT_VERSION = "v1"
STAMP_PATTERN = re.compile(r"\[Agent v\d+,[^\]]*\][^\n]*", re.MULTILINE)


# Felder, die beim Re-Read nicht vorhanden sind (z.B. lineItems ohne
# loadLineItems=true) und deshalb NICHT als silently_dropped gelten.
_SKIP_IN_DROP_CHECK: frozenset = frozenset({"lineItems"})


def normalize_fields_input(raw: dict) -> dict:
    """Akzeptiert flaches dict und Extractor-Wrapper-Format.

    1. Flat dict: ``{"documentNumber": "INV-1", ...}``
    2. Extractor-Wrapper: ``{"documentUid": ..., "extracted":
       {"documentNumber": {"value": "INV-1", "quote": "..."}}, ...}``

    Wandelt das Wrapper-Format in flach um. Felder mit ``value=None``
    werden weggelassen.
    """
    if "extracted" in raw and isinstance(raw["extracted"], dict):
        flat = {}
        for field_name, payload in raw["extracted"].items():
            if isinstance(payload, dict):
                value = payload.get("value")
                if value is None:
                    continue
                flat[field_name] = value
            else:
                flat[field_name] = payload
        return flat
    return dict(raw)


def extract_quotes(raw: dict) -> dict:
    """Holt die Quote-Strings aus dem Wrapper, für den Audit-Stempel."""
    if "extracted" not in raw or not isinstance(raw["extracted"], dict):
        return {}
    quotes = {}
    for field_name, payload in raw["extracted"].items():
        if isinstance(payload, dict) and payload.get("quote"):
            quotes[field_name] = payload["quote"]
    return quotes


def _normalize_for_compare(value):
    """Bringt einen Wert in eine vergleichbare Form (string vs. float)."""
    if value is None:
        return None
    if isinstance(value, bool):
        return value
    s = str(value).strip()
    if s == "":
        return ""
    try:
        return float(s)
    except (ValueError, TypeError):
        return s


def detect_silently_dropped(sent: dict, received: dict) -> list:
    """Findet Felder, die wir gesendet haben, die in der Re-Read-Antwort fehlen."""
    dropped = []
    for key, sent_val in sent.items():
        if key in _SKIP_IN_DROP_CHECK:
            continue
        recv_val = received.get(key)
        if recv_val is None or recv_val == "":
            dropped.append(key)
            continue
        if _normalize_for_compare(sent_val) != _normalize_for_compare(recv_val):
            dropped.append(key)
    return dropped


def make_audit_stamp(version: str, added_fields: list, critic_status: str) -> str:
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
    fields_str = ", ".join(added_fields) if added_fields else "(keine)"
    return f"[Agent {version}, {today}] Ergaenzt: {fields_str}. Critic: {critic_status}."


def merge_audit_stamp(existing_note: str, new_stamp: str) -> str:
    existing_note = existing_note or ""
    cleaned = STAMP_PATTERN.sub("", existing_note).rstrip()
    if cleaned:
        return cleaned + "\n" + new_stamp
    return new_stamp


def run(uid: int, fields_file: str, tag: str, dry_run: bool) -> int:
    try:
        from gmi_bridge import (  # noqa: WPS433
            build_client,
            fetch_one_document_meta_via_list,
            set_status_tag,
            update_document,
            UPDATE_DOCUMENT_ALLOWED_KEYS,
        )
    except (FileNotFoundError, ImportError) as e:
        print(json.dumps({"error": f"depends_on-Vertrag verletzt: {e}"}, ensure_ascii=False))
        return 3

    raw = json.loads(Path(fields_file).read_text(encoding="utf-8"))
    flat_fields = normalize_fields_input(raw)
    quotes = extract_quotes(raw)
    write_fields = {k: v for k, v in flat_fields.items() if k in UPDATE_DOCUMENT_ALLOWED_KEYS}

    if dry_run:
        print(json.dumps({
            "uid": uid,
            "dry_run": True,
            "would_write": write_fields,
            "would_tag": tag,
        }, ensure_ascii=False, indent=2))
        return 0

    try:
        client = build_client()
    except Exception:
        print(json.dumps({"error": "Bridge fehlgeschlagen."}, ensure_ascii=False))
        return 2

    try:
        update_document(client, uid, write_fields)
        re_read = fetch_one_document_meta_via_list(client, uid)
        if re_read is None:
            print(json.dumps({
                "error": f"Re-Read: UID {uid} nach Schreibvorgang nicht gefunden.",
                "uid": uid,
            }, ensure_ascii=False))
            return 1
        meta = re_read.get("meta_data", re_read) if isinstance(re_read, dict) else {}
        if not isinstance(meta, dict):
            meta = {}

        dropped = detect_silently_dropped(write_fields, meta)

        final_tag = "agent-review" if dropped else tag
        existing_tags = set_status_tag(list(meta.get("tags") or []), final_tag)

        added_fields = list(write_fields.keys())
        critic_status = "pass" if not dropped else f"fail (silently_dropped: {','.join(dropped)})"
        stamp = make_audit_stamp(AGENT_VERSION, added_fields, critic_status)

        if quotes:
            quote_summary = " | ".join(
                f"{k}: '{v[:60]}'" + ("..." if len(v) > 60 else "")
                for k, v in quotes.items() if k in write_fields
            )
            if quote_summary:
                stamp = stamp + " Quotes: " + quote_summary

        new_note = merge_audit_stamp(meta.get("note") or "", stamp)

        update_document(client, uid, {"tags": existing_tags, "note": new_note})

    except Exception as e:
        print(json.dumps({"error": f"Schreibvorgang fehlgeschlagen: {type(e).__name__}"}, ensure_ascii=False))
        return 1

    print(json.dumps({
        "uid": uid,
        "success": True,
        "silently_dropped": dropped,
        "audit_stamp": stamp,
        "final_tag": final_tag,
    }, ensure_ascii=False, indent=2))
    return 0


def main() -> int:
    p = argparse.ArgumentParser(description="gmi-accountant Write")
    p.add_argument("--uid", type=int, required=True)
    p.add_argument("--fields-file", required=True)
    p.add_argument("--tag", default="agent-done",
                   choices=["agent-done", "agent-review"])
    p.add_argument("--dry-run", action="store_true")
    args = p.parse_args()
    return run(args.uid, args.fields_file, args.tag, args.dry_run)


if __name__ == "__main__":
    sys.exit(main())
