"""Client-side wrapper for the meeting summary endpoint.

Reads a meeting's transcript.md, POSTs it to the lumo-server's
`/meetings/summarize` endpoint, and writes the returned markdown to
summary.md in the same meeting folder.

Why a separate module from meeting_transcribe / meeting_live_transcribe:
  - The transcribe path runs on-device (MLX or whisper.cpp). The
    summarise path goes over the network to the Lirelle server which calls
    OpenRouter. Different failure modes, different rate limits, different
    retry policies — keeping them in separate modules makes that
    asymmetry obvious.
  - This module is dead simple (one HTTP POST + one file write) so it's
    safe to call from anywhere, including from a tiny background thread
    fired by MeetingRecording.stop() after the transcript is ready.
"""

from __future__ import annotations

import json
import threading
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Optional

from .licensing import LICENSE_SERVER_URL, _load_session


# Long timeout — summarising an hour of dense Russian dialogue with
# Claude Sonnet 4.6 can take 60-90 seconds. The server itself caps at
# 120 s in meeting_summary_api.REQUEST_TIMEOUT_SECONDS, so we give it a
# small buffer beyond that for network round-trip.
REQUEST_TIMEOUT_SECONDS = 140.0


def _consent_file() -> Path:
    """Location of the one-time consent record (audit #5). Bank UI writes
    `{granted: true|false, at: iso}` here after showing the modal."""
    return (Path.home() / "Library" / "Application Support" / "Lumo"
            / "summary_consent.json")


def _summary_consent_granted() -> bool:
    p = _consent_file()
    if not p.exists():
        return False
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
        return bool(data.get("granted"))
    except Exception:
        return False


def _stamp_summary_failure(meta_path: Path, kind: str, detail: str) -> None:
    """CR #14 — instead of silently leaving meta.status='transcribed' and
    pretending the summary will eventually appear, mark the meeting as
    summary_failed with a short reason so the UI can render a Retry
    button instead of a perpetual 'Summary will appear here'."""
    try:
        meta = (json.loads(meta_path.read_text(encoding="utf-8"))
                if meta_path.exists() else {})
        meta["status"] = "summary_failed"
        meta["summary_error"] = f"{kind}: {detail}" if detail else kind
        from datetime import datetime as _dt
        meta["summary_failed_at"] = _dt.now().isoformat(timespec="seconds")
        meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2),
                             encoding="utf-8")
    except Exception as e:
        print(f"[summarize] couldn't stamp failure: {e}", flush=True)


def summarize_meeting(meeting_dir: Path) -> Optional[Path]:
    """Summarise the transcript at `meeting_dir/transcript.md` and write
    the result to `meeting_dir/summary.md`. Returns the summary path on
    success, None if the meeting has no transcript yet or the call fails.

    Updates meeting_dir/meta.json status to 'summarised' on success.

    Failures are logged but never raised — the meeting still works
    without a summary, and the bank UI can show the transcript alone.
    """
    transcript_path = meeting_dir / "transcript.md"
    summary_path = meeting_dir / "summary.md"
    meta_path = meeting_dir / "meta.json"

    if not transcript_path.exists():
        print(f"[summarize] no transcript at {transcript_path}", flush=True)
        return None
    if summary_path.exists():
        print(f"[summarize] summary already exists at {summary_path} — skip",
              flush=True)
        return summary_path

    transcript = transcript_path.read_text(encoding="utf-8")
    if not transcript.strip():
        print(f"[summarize] transcript is empty: {transcript_path}", flush=True)
        return None

    # Audit #5 — privacy consent gate. The summary call sends the FULL
    # transcript to our backend, which forwards it to OpenRouter / Gemini.
    # Until the user explicitly grants consent in the bank UI we keep
    # the transcript on-device and skip the network call entirely (the
    # transcript itself is still saved locally).
    if not _summary_consent_granted():
        print(f"[summarize] skipping {meeting_dir.name}: no off-device "
              f"summary consent yet", flush=True)
        try:
            meta = (json.loads(meta_path.read_text(encoding="utf-8"))
                    if meta_path.exists() else {})
            meta["status"] = "awaiting_summary_consent"
            meta["summary_skipped_reason"] = (
                "Off-device summary needs consent. Open Settings → "
                "Allow cloud summaries to enable.")
            meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2),
                                 encoding="utf-8")
        except Exception as e:
            print(f"[summarize] couldn't update meta: {e}", flush=True)
        return None

    # Short-meeting gate. Gemini hallucinates a fake dialogue ("the
    # meeting is too short, I need more details…") on tiny transcripts.
    # That wastes tokens AND fills the UI with junk. Two cheap guards:
    #   • meta.duration_seconds < 60 → skip
    #   • spoken-content chars (transcript minus the header lines we
    #     write at the top) < 200 → skip
    # Either trip writes a short marker into meta.json so the UI can
    # show "Too short to summarise" instead of an empty body.
    _MIN_DURATION_S = 60
    _MIN_SPOKEN_CHARS = 200
    try:
        _meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
    except Exception:
        _meta = {}
    _duration = int(_meta.get("duration_seconds") or 0)
    _spoken = "\n".join(
        ln for ln in transcript.splitlines()
        if ln.strip().startswith("[")  # only [MM:SS] Speaker: ... lines
    )
    if _duration and _duration < _MIN_DURATION_S:
        _reason = f"too short ({_duration}s < {_MIN_DURATION_S}s)"
    elif len(_spoken.strip()) < _MIN_SPOKEN_CHARS:
        _reason = f"too little spoken content ({len(_spoken.strip())} chars < {_MIN_SPOKEN_CHARS})"
    else:
        _reason = None
    if _reason is not None:
        print(f"[summarize] skipping {meeting_dir.name}: {_reason}", flush=True)
        try:
            _meta["status"] = "skipped_too_short"
            _meta["summary_skipped_reason"] = _reason
            meta_path.write_text(json.dumps(_meta, ensure_ascii=False, indent=2),
                                 encoding="utf-8")
        except Exception as _e:
            print(f"[summarize] couldn't update meta: {_e}", flush=True)
        return None

    session = _load_session()
    if not session or not session.get("token"):
        print("[summarize] no session token — user must log in. skipping",
              flush=True)
        return None

    url = f"{LICENSE_SERVER_URL.rstrip('/')}/meetings/summarize"
    body = json.dumps({"transcript": transcript}).encode("utf-8")
    req = urllib.request.Request(url, data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    req.add_header("Authorization", f"Bearer {session['token']}")

    print(f"[summarize] requesting summary for {meeting_dir.name} "
          f"({len(transcript)} chars)…", flush=True)
    try:
        with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
            data = resp.read()
    except urllib.error.HTTPError as e:
        detail = ""
        try:
            detail = e.read().decode("utf-8", errors="replace")
        except Exception:
            pass
        print(f"[summarize] HTTP {e.code} from server: {detail[:240]}",
              flush=True)
        _stamp_summary_failure(meta_path, f"HTTP {e.code}", detail[:240])
        return None
    except urllib.error.URLError as e:
        print(f"[summarize] network error: {e.reason}", flush=True)
        _stamp_summary_failure(meta_path, "network", str(e.reason)[:240])
        return None
    except Exception as e:
        print(f"[summarize] unexpected error: {e}", flush=True)
        _stamp_summary_failure(meta_path, type(e).__name__, str(e)[:240])
        return None


    try:
        payload = json.loads(data)
    except json.JSONDecodeError as e:
        print(f"[summarize] could not decode response: {e}", flush=True)
        return None

    if not payload.get("ok") or not isinstance(payload.get("summary"), str):
        print(f"[summarize] server returned no summary: {payload!r}",
              flush=True)
        return None

    summary = payload["summary"].strip()
    if not summary:
        print("[summarize] server returned empty summary", flush=True)
        return None

    # Atomic write — same pattern as the live transcript writer.
    tmp = summary_path.with_suffix(".md.tmp")
    try:
        tmp.write_text(summary + "\n", encoding="utf-8")
        tmp.replace(summary_path)
    except OSError as e:
        print(f"[summarize] write failed: {e}", flush=True)
        try:
            tmp.unlink()
        except OSError:
            pass
        return None

    usage = payload.get("usage") or {}
    print(f"[summarize] wrote → {summary_path}  "
          f"(model={usage.get('model')} "
          f"in={usage.get('input_tokens')} out={usage.get('output_tokens')})",
          flush=True)

    # Bump meta.json status so the bank UI can render a 'summarised' badge.
    try:
        meta = json.loads(meta_path.read_text(encoding="utf-8"))
        meta["status"] = "summarised"
        meta["summarised_at"] = datetime.now().isoformat(timespec="seconds")
        meta["summary_usage"] = usage
        meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
    except Exception as e:
        print(f"[summarize] meta update failed: {e}", flush=True)

    return summary_path


def summarize_meeting_async(meeting_dir: Path,
                            on_done: Optional[Callable[[Optional[Path]], None]] = None,
                            ) -> threading.Thread:
    """Run summarize_meeting() on a background daemon thread. `on_done`
    is called with the summary path (or None on failure) when finished —
    wire it to AppHelper.callAfter(<bank rebuild>) so the UI refreshes."""
    def _worker():
        path: Optional[Path] = None
        try:
            path = summarize_meeting(meeting_dir)
        except Exception as e:
            print(f"[summarize] worker crashed: {e}", flush=True)
        if on_done is not None:
            try:
                on_done(path)
            except Exception as e:
                print(f"[summarize] on_done callback failed: {e}", flush=True)

    t = threading.Thread(target=_worker, daemon=True,
                         name=f"meeting-summarize-{meeting_dir.name}")
    t.start()
    return t
