"""Workflow: Attachment-Verwaltung für GetMyInvoices-Rechnungen.

Subcommands:
  list        Anhänge zu einer Rechnung auflisten
  inspect     Einzel-Meta-Daten eines Anhangs (ohne Datei-Download)
  get         Anhang herunterladen (dekodiert fileContent auf Disk)
  upload      Einen einzelnen Anhang hochladen
  bulk-upload Mehrere Anhänge auf einmal hochladen (--file ... oder --dir ...)
  delete      Anhang löschen (Confirm-Flow via exakter fileName-Match)

Endpoints:
  GET    /documents/{documentUid}/attachments                       getAttachmentList
  POST   /documents/{documentUid}/attachments                       uploadOneAttachment
  GET    /documents/{documentUid}/attachments/{attachmentUid}       getOneAttachment
  DELETE /documents/{documentUid}/attachments/{attachmentUid}       deleteOneAttachment

Upload-Body: {fileName, fileContent: <base64>} (JSON, kein multipart).
Attachments nehmen NUR diese beiden Felder, keinen documentType oder description
im Body (Description wird serverseitig leer gesetzt und taucht nur beim Listing auf).

SICHERHEIT: `fileContent` (base64-Binärinhalt) wird niemals in stdout, Logs,
Preview oder Fehlermeldungen geschrieben. `inspect` nutzt `_strip_forbidden`
aus `_safety.py`. `get` dekodiert den Inhalt direkt auf Disk.

API-Einschraenkung: Die API bietet keinen `updateAttachment`-Endpoint.
Attachment-Metadaten (description) sind read-only.
"""
from __future__ import annotations

import argparse
import base64
import datetime as _dt
import json
import sys
from pathlib import Path
from typing import Any, Iterable

try:
    sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, Exception):
    pass

sys.path.insert(0, str(Path(__file__).parent.parent))

from gmi_client import GmiClient, GmiHttpError  # noqa: E402
from workflows._safety import _strip_forbidden  # noqa: E402


# ========== Fetcher ==========

def list_attachments(client: GmiClient, document_uid: int) -> dict[str, Any]:
    """Liste alle Anhänge einer Rechnung. Response: {records, totalCount}."""
    try:
        raw = client.call("getAttachmentList", documentUid=document_uid)
    except GmiHttpError as e:
        if e.status == 404:
            return {"ok": False, "error": "not_found", "documentUid": document_uid}
        return {"ok": False, "error": f"http_{e.status}", "detail": str(e),
                "documentUid": document_uid}

    if not isinstance(raw, dict):
        return {"ok": False, "error": "unexpected_response_type",
                "documentUid": document_uid}

    records = raw.get("records") or []
    return {
        "ok": True,
        "documentUid": document_uid,
        "records": records,
        "totalCount": raw.get("totalCount", len(records)),
    }


def inspect_attachment(
    client: GmiClient, document_uid: int, attachment_uid: int,
) -> dict[str, Any]:
    """Liest Meta-Daten eines einzelnen Anhangs. fileContent wird IMMER verworfen."""
    try:
        raw = client.call(
            "getOneAttachment",
            documentUid=document_uid,
            attachmentUid=attachment_uid,
        )
    except GmiHttpError as e:
        if e.status == 404:
            return {"ok": False, "error": "not_found",
                    "documentUid": document_uid, "attachmentUid": attachment_uid}
        return {"ok": False, "error": f"http_{e.status}", "detail": str(e),
                "documentUid": document_uid, "attachmentUid": attachment_uid}

    if not isinstance(raw, dict):
        return {"ok": False, "error": "unexpected_response_type"}

    # Defensiv: kompletter rekursiver Strip, danach meta extrahieren.
    safe = _strip_forbidden(raw)
    meta = safe.get("meta_data") if isinstance(safe.get("meta_data"), dict) else safe

    # API-Inkonsistenz: getOneAttachment nutzt `filename` (lowercase),
    # getAttachmentList nutzt `fileName` (camelCase). Auf `fileName`
    # normalisieren, damit Callers einheitlich arbeiten können.
    if isinstance(meta, dict) and "filename" in meta and "fileName" not in meta:
        meta = dict(meta)
        meta["fileName"] = meta.get("filename")

    return {
        "ok": True,
        "documentUid": document_uid,
        "attachmentUid": attachment_uid,
        "meta_data": meta,
    }


def get_attachment(
    client: GmiClient,
    document_uid: int,
    attachment_uid: int,
    target: Path,
    filename: str | None,
) -> dict[str, Any]:
    """Lädt einen Anhang herunter, dekodiert base64 direkt auf Disk.

    fileContent taucht NIE in Return-Werten oder Logs auf.
    """
    try:
        raw = client.call(
            "getOneAttachment",
            documentUid=document_uid,
            attachmentUid=attachment_uid,
        )
    except GmiHttpError as e:
        if e.status == 404:
            return {"ok": False, "error": "not_found",
                    "documentUid": document_uid, "attachmentUid": attachment_uid}
        return {"ok": False, "error": f"http_{e.status}", "detail": str(e)}

    if not isinstance(raw, dict):
        return {"ok": False, "error": "unexpected_response_type"}

    b64 = raw.get("fileContent")
    if not b64:
        return {"ok": False, "error": "no_file_content",
                "hint": "API-Response enthielt kein fileContent-Feld",
                "documentUid": document_uid, "attachmentUid": attachment_uid}

    # API-Inkonsistenz: getAttachmentList liefert `fileName`, getOneAttachment
    # liefert `filename` (lowercase). Beide Varianten akzeptieren.
    meta_source = raw.get("meta_data") if isinstance(raw.get("meta_data"), dict) else raw
    default_name = (
        (meta_source or {}).get("filename")
        or (meta_source or {}).get("fileName")
        or f"attachment_{attachment_uid}.bin"
    )
    out_name = filename or default_name

    try:
        target.mkdir(parents=True, exist_ok=True)
        decoded = base64.b64decode(b64)
        out_path = target / out_name
        out_path.write_bytes(decoded)
    except (OSError, ValueError) as e:
        return {"ok": False, "error": "write_failed", "detail": str(e)}

    # WICHTIG: weder b64 noch `decoded` landen im Return.
    return {
        "ok": True,
        "action": "get",
        "documentUid": document_uid,
        "attachmentUid": attachment_uid,
        "filename": out_name,
        "path": str(out_path),
        "bytes": len(decoded),
    }


# ========== Writes ==========

def upload_attachment(
    client: GmiClient, document_uid: int, file_path: Path,
) -> dict[str, Any]:
    """Lädt eine einzelne Datei als Anhang zu einer Rechnung hoch."""
    path = Path(file_path)
    if not path.is_file():
        return {"ok": False, "error": "file_not_found", "path": str(path)}

    raw_bytes = path.read_bytes()
    body = {
        "fileName": path.name,
        "fileContent": base64.b64encode(raw_bytes).decode("ascii"),
    }

    try:
        resp = client.call(
            "uploadOneAttachment", documentUid=document_uid, _body=body,
        )
    except GmiHttpError as e:
        return {"ok": False, "error": f"http_{e.status}", "detail": str(e),
                "fileName": path.name}

    att_uid = None
    if isinstance(resp, dict):
        att_uid = resp.get("attachmentUid") or resp.get("uid")

    return {
        "ok": True,
        "action": "upload",
        "documentUid": document_uid,
        "attachmentUid": att_uid,
        "fileName": path.name,
        "fileSize": len(raw_bytes),
        "response": resp,
    }


def collect_files_from_dir(directory: Path, pattern: str = "*") -> list[Path]:
    """Liefert alle Dateien im Verzeichnis (nicht rekursiv), die dem Muster entsprechen."""
    directory = Path(directory)
    if not directory.is_dir():
        return []
    return sorted(p for p in directory.glob(pattern) if p.is_file())


def bulk_upload_attachments(
    client: GmiClient, document_uid: int, files: Iterable[Path],
) -> dict[str, Any]:
    """Lädt mehrere Dateien sequentiell als Anhänge hoch.

    Gibt für jedes File das Einzelresult zurück, aggregiert Counters.
    Ein Fehler stoppt den Vorgang nicht (continue-on-error).
    """
    files_list = list(files)
    if not files_list:
        return {"ok": False, "error": "no_files",
                "hint": "Keine Dateien zum Hochladen gefunden"}

    results = []
    uploaded = 0
    failed = 0
    for path in files_list:
        res = upload_attachment(client, document_uid, path)
        results.append(res)
        if res.get("ok"):
            uploaded += 1
        else:
            failed += 1

    return {
        "ok": True,
        "action": "bulk-upload",
        "documentUid": document_uid,
        "total": len(files_list),
        "uploaded": uploaded,
        "failed": failed,
        "results": results,
    }


def delete_attachment(
    client: GmiClient,
    document_uid: int,
    attachment_uid: int,
    confirm_filename: str | None,
    interactive: bool,
) -> dict[str, Any]:
    """Löscht einen Anhang mit zweistufigem Confirm-Flow.

    Layer 1: Preview via getAttachmentList, suche nach attachment_uid.
    Layer 2: Bestätigung via exakter fileName-Match (interaktiv oder --confirm-filename).
    """
    # Layer 1: Preview
    lst = list_attachments(client, document_uid)
    if not lst.get("ok"):
        return lst

    target = None
    for rec in lst["records"]:
        if rec.get("attachmentUid") == attachment_uid:
            target = rec
            break

    if target is None:
        return {"ok": False, "error": "not_found",
                "documentUid": document_uid, "attachmentUid": attachment_uid,
                "hint": "attachmentUid nicht in getAttachmentList gefunden"}

    actual_filename = (target.get("fileName") or "").strip()

    preview_slim = {
        "documentUid": document_uid,
        "attachmentUid": attachment_uid,
        "fileName": actual_filename,
        "description": target.get("description", ""),
        "attachedAt": target.get("attachedAt"),
    }

    # Layer 2: Bestätigung
    if interactive:
        prompt = (f"\nZu löschen: documents/{document_uid}/attachments/{attachment_uid}\n"
                  f"  fileName:    {actual_filename!r}\n"
                  f"  description: {target.get('description', '')!r}\n"
                  f"  attachedAt:  {target.get('attachedAt')!r}\n"
                  f"Zur Bestätigung bitte den EXAKTEN fileName eintippen "
                  f"(z.B. {actual_filename!r}): ")
        try:
            typed = input(prompt)
        except EOFError:
            return {"ok": False, "error": "not_confirmed", "reason": "no_tty_input",
                    "preview": preview_slim}
        typed = typed.strip()
        if typed == "":
            return {"ok": False, "error": "not_confirmed", "preview": preview_slim}
        if typed != actual_filename:
            return {"ok": False, "error": "mismatch", "expected": actual_filename,
                    "got": typed, "preview": preview_slim}
    else:
        if not confirm_filename:
            return {"ok": False, "error": "missing_confirm",
                    "hint": ("Non-interaktiv --confirm UND --confirm-filename "
                             "<exakter fileName> setzen"),
                    "preview": preview_slim}
        if confirm_filename != actual_filename:
            return {"ok": False, "error": "filename_mismatch",
                    "expected": actual_filename, "got": confirm_filename,
                    "documentUid": document_uid, "attachmentUid": attachment_uid}

    # Execute
    try:
        resp = client.call(
            "deleteOneAttachment",
            documentUid=document_uid,
            attachmentUid=attachment_uid,
        )
    except GmiHttpError as e:
        return {"ok": False, "error": f"http_{e.status}", "detail": str(e),
                "documentUid": document_uid, "attachmentUid": attachment_uid}

    ts = _dt.datetime.now().isoformat(timespec="seconds")
    print(
        f"[DELETE] documents/{document_uid}/attachments/{attachment_uid} "
        f"\"{actual_filename}\" at {ts}",
        file=sys.stderr,
    )

    return {
        "ok": True,
        "action": "delete",
        "documentUid": document_uid,
        "attachmentUid": attachment_uid,
        "fileName": actual_filename,
        "response": resp,
    }


# ========== Formatter ==========

def format_list_md(data: dict) -> str:
    if not data.get("ok"):
        return format_error_md(data)
    uid = data.get("documentUid")
    recs = data.get("records", [])
    if not recs:
        return f"## Anhänge zu Rechnung {uid}\n\nKeine Anhänge vorhanden."

    lines = [f"## Anhänge zu Rechnung {uid} ({data.get('totalCount', len(recs))})", ""]
    lines.append("| attachmentUid | fileName | description | attachedAt |")
    lines.append("|---:|---|---|---|")
    for r in recs:
        lines.append(
            f"| {r.get('attachmentUid', '')} "
            f"| {r.get('fileName', '')} "
            f"| {r.get('description', '')} "
            f"| {r.get('attachedAt', '')} |"
        )
    return "\n".join(lines)


def format_inspect_md(data: dict) -> str:
    if not data.get("ok"):
        return format_error_md(data)
    meta = data.get("meta_data", {}) or {}
    lines = [
        f"## Anhang {data['attachmentUid']} (Rechnung {data['documentUid']})", "",
    ]
    for label, key in [
        ("fileName", "fileName"),
        ("description", "description"),
        ("attachedAt", "attachedAt"),
        ("size", "fileSize"),
        ("mimeType", "mimeType"),
    ]:
        if meta.get(key) not in (None, ""):
            lines.append(f"- **{label}:** {meta[key]}")
    return "\n".join(lines)


def format_write_md(data: dict) -> str:
    if not data.get("ok"):
        return format_error_md(data)
    action = data.get("action", "?")
    lines = [f"## {action} — OK", ""]
    for key in ("documentUid", "attachmentUid", "fileName", "fileSize",
                "filename", "path", "bytes"):
        if data.get(key) not in (None, ""):
            lines.append(f"- **{key}:** {data[key]}")
    if action == "bulk-upload":
        lines.append(f"- **total:** {data.get('total')}")
        lines.append(f"- **uploaded:** {data.get('uploaded')}")
        lines.append(f"- **failed:** {data.get('failed')}")
        lines.append("")
        lines.append("### Einzelergebnisse")
        for r in data.get("results", []):
            ok = "OK" if r.get("ok") else f"FAIL ({r.get('error', '?')})"
            lines.append(f"- {r.get('fileName', '?')}: {ok}")
    return "\n".join(lines)


def format_error_md(data: dict) -> str:
    return f"## Fehler\n\n```json\n{json.dumps(data, indent=2, ensure_ascii=False, default=str)}\n```"


def _print(data: dict, args: argparse.Namespace) -> None:
    if args.format == "json":
        print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
        return
    if args.cmd == "list":
        print(format_list_md(data))
    elif args.cmd == "inspect":
        print(format_inspect_md(data))
    else:
        print(format_write_md(data))


# ========== CLI ==========

def main() -> int:
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--format", choices=["json", "markdown"], default="markdown")

    p = argparse.ArgumentParser(
        parents=[common],
        description="Attachment-Verwaltung für GetMyInvoices-Rechnungen",
    )
    sub = p.add_subparsers(dest="cmd", required=True)

    # list
    lp = sub.add_parser("list", parents=[common], help="Anhänge einer Rechnung auflisten")
    lp.add_argument("--document-uid", dest="document_uid", type=int, required=True)

    # inspect
    ip = sub.add_parser("inspect", parents=[common],
                        help="Meta-Daten eines Anhangs anzeigen (ohne Download)")
    ip.add_argument("--document-uid", dest="document_uid", type=int, required=True)
    ip.add_argument("--attachment-uid", dest="attachment_uid", type=int, required=True)

    # get
    gp = sub.add_parser("get", parents=[common],
                        help="Anhang herunterladen (fileContent wird auf Disk dekodiert)")
    gp.add_argument("--document-uid", dest="document_uid", type=int, required=True)
    gp.add_argument("--attachment-uid", dest="attachment_uid", type=int, required=True)
    gp.add_argument("--target", required=True, help="Zielordner")
    gp.add_argument("--filename", help="Optional: abweichender Dateiname")

    # upload
    up = sub.add_parser("upload", parents=[common], help="Einzelnen Anhang hochladen")
    up.add_argument("--document-uid", dest="document_uid", type=int, required=True)
    up.add_argument("--file", required=True, help="Pfad zur Datei")

    # bulk-upload
    bp = sub.add_parser("bulk-upload", parents=[common],
                        help="Mehrere Anhänge hochladen (--file wiederholt oder --dir)")
    bp.add_argument("--document-uid", dest="document_uid", type=int, required=True)
    bp.add_argument("--file", dest="files", action="append", default=[],
                    help="Pfad zu einer Datei (wiederholbar)")
    bp.add_argument("--dir", help="Verzeichnis mit Dateien")
    bp.add_argument("--pattern", default="*",
                    help="Glob-Pattern bei --dir (Default: *)")

    # delete
    dp = sub.add_parser("delete", parents=[common],
                        help="Anhang löschen (Confirm-Pflicht)")
    dp.add_argument("--document-uid", dest="document_uid", type=int, required=True)
    dp.add_argument("--attachment-uid", dest="attachment_uid", type=int, required=True)
    dp.add_argument("--confirm", action="store_true",
                    help="Non-interaktiv: zusammen mit --confirm-filename")
    dp.add_argument("--confirm-filename", dest="confirm_filename",
                    help="Exakter fileName des Anhangs")

    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 == "list":
            data = list_attachments(client, args.document_uid)
        elif args.cmd == "inspect":
            data = inspect_attachment(client, args.document_uid, args.attachment_uid)
        elif args.cmd == "get":
            data = get_attachment(
                client,
                document_uid=args.document_uid,
                attachment_uid=args.attachment_uid,
                target=Path(args.target),
                filename=args.filename,
            )
        elif args.cmd == "upload":
            data = upload_attachment(client, args.document_uid, Path(args.file))
        elif args.cmd == "bulk-upload":
            files: list[Path] = [Path(f) for f in (args.files or [])]
            if args.dir:
                files.extend(collect_files_from_dir(Path(args.dir), args.pattern))
            # Dedupe, preserve order
            seen = set()
            uniq: list[Path] = []
            for f in files:
                key = str(f.resolve())
                if key not in seen:
                    seen.add(key)
                    uniq.append(f)
            data = bulk_upload_attachments(client, args.document_uid, uniq)
        elif args.cmd == "delete":
            interactive = sys.stdin.isatty() and not args.confirm
            data = delete_attachment(
                client,
                document_uid=args.document_uid,
                attachment_uid=args.attachment_uid,
                confirm_filename=args.confirm_filename,
                interactive=interactive,
            )
        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") else 1


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