"""Workflow: Bank-Transaktionen listen, lesen, zuweisen, taggen, ignorieren.

Aufruf:
  python bank_transactions.py accounts
  python bank_transactions.py list --account <bankAccountUid> --from 2026-01-01 --to 2026-04-30
  python bank_transactions.py list --account <bankAccountUid> --text "Amazon" --max 50
  python bank_transactions.py get --account <bankAccountUid> --tx <transactionUid>
  python bank_transactions.py assign --account <bankAccountUid> --tx <transactionUid> --document <documentUid>
  python bank_transactions.py ignore --account <bankAccountUid> --tx <transactionUid> --comment "Privatbuchung"
  python bank_transactions.py unignore --account <bankAccountUid> --tx <transactionUid>
  python bank_transactions.py tag --account <bankAccountUid> --tx <transactionUid> --tags "reise,2026"

ALLE Subcommands prüfen zuerst per getAccountDetails, ob hasBankingAccess=true.
Ohne Banking-Paket wird sauber abgebrochen, kein API-Call geht raus.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

# UTF-8 stdout (Windows cp1252-Workaround, siehe lookup_masterdata.py)
try:
    sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, Exception):
    pass

# Parent-Ordner zum Pfad für gmi_client Import
sys.path.insert(0, str(Path(__file__).parent.parent))

from gmi_client import GmiClient, GmiHttpError  # noqa: E402


# ---------- Banking-Permission-Gate ----------

def require_banking(client: GmiClient) -> tuple[bool, str]:
    """Prüft hasBankingAccess. Liefert (ok, reason)."""
    try:
        acc = client.call("getAccountDetails") or {}
    except GmiHttpError as e:
        return False, f"account_lookup_failed:{e.status}"
    if not acc.get("hasBankingAccess"):
        return False, "hasBankingAccess=false"
    return True, ""


def _locked(reason: str, hint: str | None = None) -> dict:
    """Banking-Endpoint blockiert. Drei Gruende möglich:

    - hasBankingAccess=false: der API-Key hat keine Banking-Permission
      (in GetMyInvoices kann jeder Key wahlweise mit oder ohne Banking
      ausgestellt werden — Hinweis 2026-04-07).
    - http_403: API meldet 403 trotz hasBankingAccess=true. Wahrscheinlichste
      Ursache: Key-Mismatch (z.B. ein anderer Key in der Env-Var aktiv).
    - account_lookup_failed: getAccountDetails selbst hat geknallt — vorher
      Healthcheck prüfen.
    """
    if hint is None:
        if reason == "hasBankingAccess=false":
            hint = (
                "Der aktive API-Key hat keine Banking-Permission. "
                "In GetMyInvoices unter Einstellungen den Key mit Banking-Zugang ausstellen "
                "und in der Windows-User-Env-Var GETMYINVOICES_API_KEY hinterlegen."
            )
        elif reason == "http_403_unexpected":
            hint = (
                "Die API hat 403 zurückgegeben, obwohl getAccountDetails hasBankingAccess=true meldet. "
                "Möglicher Key-Mismatch: prüfe, ob in GETMYINVOICES_API_KEY tatsächlich ein Key mit "
                "Banking-Permission liegt."
            )
        elif reason.startswith("account_lookup_failed"):
            hint = "getAccountDetails selbst ist fehlgeschlagen. Healthcheck zuerst prüfen."
        else:
            hint = "Banking-Endpoint ist blockiert. Reason siehe oben."
    return {
        "ok": False,
        "locked": True,
        "reason": reason,
        "hint": hint,
    }


def _no_accounts_connected(scope: str) -> dict:
    """Banking-Permission liegt vor, aber es ist noch kein Bankkonto in
    GetMyInvoices verbunden. Live verifiziert 2026-04-07: die API antwortet
    in diesem Fall mit HTTP 422 und Code 145 ('Bank account not found.')
    statt mit einer leeren Liste.

    scope: 'accounts' oder 'transactions' — bestimmt, welches Feld leer
    zurückkommt, damit die Formatter nichts crashen.
    """
    base = {
        "ok": True,
        "no_accounts_connected": True,
        "hint": (
            "Der aktive API-Key hat Banking-Permission, aber es ist noch kein Bankkonto "
            "in GetMyInvoices verbunden. Im Web-UI unter 'Bankkonten' eines hinzufügen, "
            "dann erneut versuchen."
        ),
    }
    if scope == "transactions":
        base["transactions"] = []
        base["count"] = 0
    else:
        base["accounts"] = []
    return base


def _is_no_accounts_error(e: GmiHttpError) -> bool:
    """422 mit code 145 ('Bank account not found.') ist der GMI-spezifische
    Fall 'Banking aktiv, aber kein Konto verbunden'."""
    return e.status == 422 and e.code == 145


# ---------- Fetch-Helper ----------

def list_accounts(client: GmiClient) -> dict:
    """Bank-Konten listen (mit Gate)."""
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)
    try:
        r = client.call("listBankAccounts")
    except GmiHttpError as e:
        if _is_no_accounts_error(e):
            return _no_accounts_connected("accounts")
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    items = r if isinstance(r, list) else (r.get("data") if isinstance(r, dict) else [])
    return {"ok": True, "accounts": items or []}


def list_transactions(
    client: GmiClient,
    bank_account_uid: int,
    date_from: str | None = None,
    date_to: str | None = None,
    text: str | None = None,
    status: str | None = None,
    max_items: int | None = None,
) -> dict:
    """Transaktionen eines Kontos listen mit offset/limit-Pagination."""
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)

    params: dict[str, Any] = {"bankAccountUid": bank_account_uid}
    if date_from:
        params["startDateFilter"] = date_from
    if date_to:
        params["endDateFilter"] = date_to
    if text:
        params["textFilter"] = text
    if status:
        params["statusFilter"] = status

    try:
        items = list(client.paginate_offset(
            "listTransaction",
            items_key="records",
            limit=100,
            max_items=max_items,
            **params,
        ))
    except GmiHttpError as e:
        if _is_no_accounts_error(e):
            data = _no_accounts_connected("transactions")
            data["bankAccountUid"] = bank_account_uid
            return data
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    return {"ok": True, "bankAccountUid": bank_account_uid, "count": len(items), "transactions": items}


def get_transaction(client: GmiClient, bank_account_uid: int, transaction_uid: int) -> dict:
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)
    try:
        r = client.call("getTransaction", bankAccountUid=bank_account_uid, transactionUid=transaction_uid)
    except GmiHttpError as e:
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    return {"ok": True, "transaction": r}


def assign_transaction(
    client: GmiClient,
    bank_account_uid: int,
    transaction_uid: int,
    document_uid: int,
) -> dict:
    """Verknuepft eine Transaktion mit einem Dokument."""
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)
    try:
        r = client.call(
            "assignTransaction",
            bankAccountUid=bank_account_uid,
            transactionUid=transaction_uid,
            _body={"documentUid": document_uid},
        )
    except GmiHttpError as e:
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    return {"ok": True, "response": r}


def ignore_transaction(
    client: GmiClient,
    bank_account_uid: int,
    transaction_uid: int,
    ignore: bool,
    comment: str | None = None,
) -> dict:
    """Setzt die Transaktion auf ignored (True) oder unknown (False)."""
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)
    body: dict[str, Any] = {"ignore": bool(ignore)}
    if comment is not None:
        body["ignoreComment"] = comment
    try:
        r = client.call(
            "ignoreTransaction",
            bankAccountUid=bank_account_uid,
            transactionUid=transaction_uid,
            _body=body,
        )
    except GmiHttpError as e:
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    return {"ok": True, "response": r}


def set_transaction_tags(
    client: GmiClient,
    bank_account_uid: int,
    transaction_uid: int,
    tags: list[str],
) -> dict:
    ok, reason = require_banking(client)
    if not ok:
        return _locked(reason)
    try:
        r = client.call(
            "setTransactionTags",
            bankAccountUid=bank_account_uid,
            transactionUid=transaction_uid,
            _body={"tags": tags},
        )
    except GmiHttpError as e:
        if e.status == 403:
            return _locked("http_403_unexpected")
        raise
    return {"ok": True, "response": r}


# ---------- Formatting ----------

def format_accounts_md(items: list[dict], no_accounts_connected: bool = False) -> str:
    if no_accounts_connected:
        return (
            "## Bankkonten\n\n"
            "Es ist noch **kein Bankkonto** in GetMyInvoices verbunden.\n\n"
            "Der aktive API-Key hat zwar Banking-Permission, aber im GMI-Web-UI unter "
            "'Bankkonten' wurde noch keines hinzugefuegt. Erst dann liefern die "
            "Bank-Endpoints echte Daten."
        )
    if not items:
        return "## Bankkonten\n\n(keine)"
    lines = [f"## Bankkonten ({len(items)})", "",
             "| bankAccountUid | Name | Bank | IBAN | Saldo |",
             "|---|---|---|---|---|"]
    for b in items:
        lines.append("| {uid} | {name} | {bank} | {iban} | {bal} |".format(
            uid=b.get("bankAccountUid") or b.get("uid") or "",
            name=b.get("name") or b.get("accountName") or "",
            bank=b.get("bankName") or b.get("bank") or "",
            iban=b.get("iban") or "",
            bal=b.get("balance") or "",
        ))
    return "\n".join(lines)


def format_transactions_md(data: dict) -> str:
    items = data.get("transactions", [])
    header = f"## Transaktionen ({data.get('count', 0)}) — Konto {data.get('bankAccountUid','?')}"
    if data.get("no_accounts_connected"):
        return (
            header + "\n\n"
            "Es ist noch **kein Bankkonto** in GetMyInvoices verbunden. "
            "Erst nach dem Verbinden im Web-UI liefert die API Transaktionen."
        )
    if not items:
        return header + "\n\n(keine Treffer)"
    lines = [header, "",
             "| Datum | Beschreibung | Partner | Betrag | Währung | Status | transactionUid |",
             "|---|---|---|---|---|---|---|"]
    for t in items:
        lines.append("| {date} | {desc} | {partner} | {amount} | {cur} | {status} | {uid} |".format(
            date=t.get("bookingDate", ""),
            desc=(t.get("description") or "")[:60],
            partner=t.get("paymentPartnerName") or "",
            amount=t.get("amount", ""),
            cur=t.get("currencyCode", ""),
            status=t.get("status", ""),
            uid=t.get("transactionUid", ""),
        ))
    return "\n".join(lines)


def _locked_md(data: dict) -> str:
    reason = data.get("reason", "?")
    hint = data.get("hint", "")
    return f"## Gesperrt\n\n**Reason:** {reason}\n\n{hint}"


# ---------- CLI ----------

def _print(data: dict, args: argparse.Namespace) -> None:
    if args.format == "json":
        print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
        return
    # markdown
    if data.get("locked"):
        print(_locked_md(data))
        return
    if "accounts" in data:
        print(format_accounts_md(
            data["accounts"],
            no_accounts_connected=bool(data.get("no_accounts_connected")),
        ))
    elif "transactions" in data:
        print(format_transactions_md(data))
    elif "transaction" in data:
        # Einzelne Transaktion: als einfache Key-Value-Liste
        t = data["transaction"]
        if isinstance(t, dict):
            lines = [f"## Transaktion {t.get('transactionUid','?')}", ""]
            for k in ("bookingDate", "description", "paymentPartnerName", "amount", "currencyCode",
                      "status", "creditorId", "tags", "ignoreComment", "assignedDocuments"):
                if k in t and t[k] not in (None, "", []):
                    lines.append(f"- **{k}:** {t[k]}")
            print("\n".join(lines))
        else:
            print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
    elif "response" in data:
        print(f"OK — API-Response:\n{json.dumps(data['response'], indent=2, ensure_ascii=False, default=str)}")
    else:
        print(json.dumps(data, indent=2, ensure_ascii=False, default=str))


def main() -> int:
    # Parent-Parser mit --format, den alle Subcommands erben
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--format", choices=["json", "markdown"], default="markdown")

    p = argparse.ArgumentParser(parents=[common])
    sub = p.add_subparsers(dest="cmd", required=True)

    sub.add_parser("accounts", parents=[common], help="Bank-Konten listen")

    lt = sub.add_parser("list", parents=[common], help="Transaktionen eines Kontos listen")
    lt.add_argument("--account", type=int, required=True, help="bankAccountUid")
    lt.add_argument("--from", dest="date_from", help="Startdatum YYYY-MM-DD")
    lt.add_argument("--to", dest="date_to", help="Enddatum YYYY-MM-DD")
    lt.add_argument("--text", help="Substring-Filter auf Beschreibung")
    lt.add_argument("--status", help="Status-Filter (z.B. assigned, ignored, unknown)")
    lt.add_argument("--max", dest="max_items", type=int, help="Max. Transaktionen")

    gt = sub.add_parser("get", parents=[common], help="Einzelne Transaktion lesen")
    gt.add_argument("--account", type=int, required=True)
    gt.add_argument("--tx", type=int, required=True, help="transactionUid")

    at = sub.add_parser("assign", parents=[common], help="Transaktion einem Dokument zuweisen")
    at.add_argument("--account", type=int, required=True)
    at.add_argument("--tx", type=int, required=True)
    at.add_argument("--document", type=int, required=True, help="documentUid")

    ig = sub.add_parser("ignore", parents=[common], help="Transaktion als ignoriert markieren")
    ig.add_argument("--account", type=int, required=True)
    ig.add_argument("--tx", type=int, required=True)
    ig.add_argument("--comment", help="Optionaler Kommentar")

    un = sub.add_parser("unignore", parents=[common], help="Ignorieren aufheben")
    un.add_argument("--account", type=int, required=True)
    un.add_argument("--tx", type=int, required=True)

    tg = sub.add_parser("tag", parents=[common], help="Tags einer Transaktion setzen (ersetzt bestehende)")
    tg.add_argument("--account", type=int, required=True)
    tg.add_argument("--tx", type=int, required=True)
    tg.add_argument("--tags", required=True, help="Kommagetrennte Liste, z.B. 'reise,2026'")

    args = p.parse_args()

    try:
        client = GmiClient()
    except Exception as e:
        print(json.dumps({"error": "config", "detail": str(e)}))
        return 1

    try:
        if args.cmd == "accounts":
            data = list_accounts(client)
        elif args.cmd == "list":
            data = list_transactions(
                client, args.account,
                date_from=args.date_from, date_to=args.date_to,
                text=args.text, status=args.status, max_items=args.max_items,
            )
        elif args.cmd == "get":
            data = get_transaction(client, args.account, args.tx)
        elif args.cmd == "assign":
            data = assign_transaction(client, args.account, args.tx, args.document)
        elif args.cmd == "ignore":
            data = ignore_transaction(client, args.account, args.tx, ignore=True, comment=args.comment)
        elif args.cmd == "unignore":
            data = ignore_transaction(client, args.account, args.tx, ignore=False)
        elif args.cmd == "tag":
            tags = [t.strip() for t in args.tags.split(",") if t.strip()]
            data = set_transaction_tags(client, args.account, args.tx, tags)
        else:
            print(json.dumps({"error": "unknown_command", "cmd": args.cmd}))
            return 1
    except GmiHttpError as e:
        print(json.dumps({"error": "api", "status": e.status, "detail": str(e)}))
        return 1

    _print(data, args)
    return 0 if data.get("ok", False) or data.get("locked") else 1


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