"""
Plaud API Client — Unofficial wrapper for the Plaud web API.

Pulls transcripts, AI summaries, and recording metadata from your Plaud Note
device. Authenticates with a bearer token extracted from web.plaud.ai.

Usage:
    plaud-client list [--json] [--limit N]
    plaud-client details <file_id> [--json]
    plaud-client search <query> [--json]
    plaud-client export [--output-dir DIR] [--format txt|md|json]
    plaud-client devices [--json]
    plaud-client test

Environment variables (via .env file):
    PLAUD_TOKEN       — Full bearer token including "bearer " prefix
    PLAUD_API_DOMAIN  — API base URL (default: https://api.plaud.ai)
"""

from __future__ import annotations

import argparse
import gzip
import json
import os
import sys
import time
import urllib.request
import urllib.parse
import urllib.error
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

DEFAULT_API_DOMAIN = "https://api.plaud.ai"
MAX_RETRIES = 3
INITIAL_RETRY_DELAY = 1.0  # seconds


def load_env(env_path: str | None = None) -> dict[str, str]:
    """Load .env file from given path or search common locations."""
    search_paths = []
    if env_path:
        search_paths.append(Path(env_path))
    search_paths.extend([
        Path.cwd() / ".env",
        Path.home() / ".config" / "plaud" / ".env",
        Path(__file__).resolve().parent.parent / ".env",
        Path.home() / ".claude" / "skills" / "plaud-transcripts" / ".env",  # legacy
    ])

    for p in search_paths:
        if p.exists():
            env = {}
            with open(p) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        key, _, value = line.partition("=")
                        env[key.strip()] = value.strip()
            return env
    return {}


# ---------------------------------------------------------------------------
# API Client
# ---------------------------------------------------------------------------

class PlaudClient:
    """Wraps the unofficial Plaud web API endpoints."""

    def __init__(self, token: str, api_domain: str = DEFAULT_API_DOMAIN):
        # Normalize token — ensure it does NOT have "bearer " prefix for the header
        # because we add "Bearer " ourselves
        if token.lower().startswith("bearer "):
            token = token[7:]
        self.token = token
        self.api_domain = api_domain.rstrip("/")

    # -- Low-level request with retry ------------------------------------------

    def _request(self, endpoint: str, method: str = "GET",
                 body: dict | None = None, retries: int = 0) -> dict:
        """Make an authenticated request with exponential-backoff retry."""
        url = f"{self.api_domain}{endpoint}"
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json",
            "Origin": "https://web.plaud.ai",
            "Referer": "https://web.plaud.ai/",
            "Accept": "application/json, text/plain, */*",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
        }

        data = json.dumps(body).encode() if body else None
        req = urllib.request.Request(url, data=data, headers=headers, method=method)

        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.HTTPError as e:
            status = e.code
            # Rate-limited — back off and retry
            if status == 429 and retries < MAX_RETRIES:
                retry_after = e.headers.get("Retry-After")
                delay = (int(retry_after) if retry_after
                         else INITIAL_RETRY_DELAY * (2 ** retries))
                time.sleep(delay)
                return self._request(endpoint, method, body, retries + 1)

            # Server error — retry
            if 500 <= status < 600 and retries < MAX_RETRIES:
                time.sleep(INITIAL_RETRY_DELAY * (2 ** retries))
                return self._request(endpoint, method, body, retries + 1)

            # Read error body for a useful message
            try:
                err_body = json.loads(e.read().decode())
                msg = err_body.get("msg", e.reason)
            except Exception:
                msg = e.reason
            raise SystemExit(f"Plaud API error ({status}): {msg}")

        except urllib.error.URLError as e:
            if retries < MAX_RETRIES:
                time.sleep(INITIAL_RETRY_DELAY * (2 ** retries))
                return self._request(endpoint, method, body, retries + 1)
            raise SystemExit(f"Network error: {e.reason}")

    # -- Public API methods ----------------------------------------------------

    def list_devices(self) -> dict:
        """GET /device/list — list connected Plaud devices."""
        return self._request("/device/list")

    def list_recordings(self, skip: int = 0, limit: int = 99999,
                        is_trash: int = 0, sort_by: str = "edit_time",
                        is_desc: bool = True) -> dict:
        """GET /file/simple/web — list recordings with metadata."""
        params = urllib.parse.urlencode({
            "skip": skip,
            "limit": limit,
            "is_trash": is_trash,
            "sort_by": sort_by,
            "is_desc": str(is_desc).lower(),
        })
        return self._request(f"/file/simple/web?{params}")

    def get_detail(self, file_id: str) -> dict:
        """GET /file/detail/{file_id} — full details with transcript + summary."""
        return self._request(f"/file/detail/{file_id}")

    def test_connection(self) -> bool:
        """Quick connectivity check using /device/list."""
        try:
            resp = self.list_devices()
            return resp.get("status") == 0 or "data_devices" in resp
        except SystemExit as e:
            print(f"\n  DEBUG: {e}", file=sys.stderr)
            return False

    # -- Convenience helpers ---------------------------------------------------

    def get_transcript(self, file_id: str) -> str | None:
        """Pull just the transcript text for a recording."""
        detail = self.get_detail(file_id)
        data = detail.get("data", detail)
        return _extract_transcript_sync(data.get("content_list", []))

    def get_summary(self, file_id: str) -> str | None:
        """Pull just the AI summary for a recording."""
        detail = self.get_detail(file_id)
        data = detail.get("data", detail)
        return _extract_summary_sync(data.get("content_list", []))


# ---------------------------------------------------------------------------
# Content fetching helpers (S3 download + decompress)
# ---------------------------------------------------------------------------

def _fetch_content_link_sync(url: str) -> dict | list | None:
    """Download a pre-signed S3 URL, decompress gzip, and parse JSON."""
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read()
            decompressed = gzip.decompress(raw)
            return json.loads(decompressed)
    except Exception:
        return None


def _format_ms_timestamp(ms: int) -> str:
    """Format milliseconds as MM:SS."""
    total_secs = ms // 1000
    minutes, seconds = divmod(total_secs, 60)
    return f"{minutes:02d}:{seconds:02d}"


def _extract_transcript_sync(content_list: list) -> str | None:
    """Find and download the transcript from content_list."""
    for item in content_list:
        if item.get("data_type") == "transaction" and item.get("task_status") == 1:
            url = item.get("data_link")
            if not url:
                continue
            segments = _fetch_content_link_sync(url)
            if not segments or not isinstance(segments, list):
                return None
            lines = []
            for seg in segments:
                speaker = seg.get("speaker") or seg.get("original_speaker") or "Unknown"
                content = seg.get("content", "")
                ts = _format_ms_timestamp(seg.get("start_time", 0))
                lines.append(f"{speaker} ({ts}): {content}")
            return "\n\n".join(lines) if lines else None
    return None


def _extract_summary_sync(content_list: list) -> str | None:
    """Find and download the AI summary from content_list."""
    for item in content_list:
        if item.get("data_type") == "auto_sum_note" and item.get("task_status") == 1:
            url = item.get("data_link")
            if not url:
                continue
            data = _fetch_content_link_sync(url)
            if not data or not isinstance(data, dict):
                return None
            return data.get("ai_content")
    return None


# ---------------------------------------------------------------------------
# CLI Helpers
# ---------------------------------------------------------------------------

def ms_to_datetime(ms: int) -> str:
    """Convert millisecond timestamp to readable datetime string."""
    if not ms:
        return "N/A"
    return datetime.fromtimestamp(ms / 1000).strftime("%Y-%m-%d %I:%M %p %Z")


def ms_to_duration(ms: int) -> str:
    """Convert duration in ms to human-readable format."""
    if not ms:
        return "N/A"
    total_secs = ms // 1000
    hours, remainder = divmod(total_secs, 3600)
    minutes, seconds = divmod(remainder, 60)
    if hours:
        return f"{hours}h {minutes}m {seconds}s"
    if minutes:
        return f"{minutes}m {seconds}s"
    return f"{seconds}s"


def print_recordings_table(recordings: list[dict]) -> None:
    """Pretty-print recordings as a table."""
    if not recordings:
        print("No recordings found.")
        return

    # Header
    print(f"{'ID':<40} {'Filename':<35} {'Date':<22} {'Duration':<12} {'Trans':<6} {'Summary':<7}")
    print("-" * 125)

    for r in recordings:
        rid = r.get("id", "?")[:38]
        name = (r.get("filename") or "Untitled")[:33]
        date = ms_to_datetime(r.get("start_time", 0))[:20]
        dur = ms_to_duration(r.get("duration", 0))
        has_trans = "Yes" if r.get("is_trans") else "No"
        has_summary = "Yes" if r.get("is_summary") else "No"
        print(f"{rid:<40} {name:<35} {date:<22} {dur:<12} {has_trans:<6} {has_summary:<7}")

    print(f"\nTotal: {len(recordings)} recording(s)")


# ---------------------------------------------------------------------------
# CLI Commands
# ---------------------------------------------------------------------------

def cmd_list(client: PlaudClient, args: argparse.Namespace) -> None:
    resp = client.list_recordings(limit=args.limit)

    if args.json:
        print(json.dumps(resp, indent=2, ensure_ascii=False))
        return

    recordings = resp.get("data_file_list", [])
    total = resp.get("data_file_total", len(recordings))
    print(f"Found {total} recording(s):\n")
    print_recordings_table(recordings)


def cmd_details(client: PlaudClient, args: argparse.Namespace) -> None:
    resp = client.get_detail(args.file_id)

    if args.json:
        print(json.dumps(resp, indent=2, ensure_ascii=False))
        return

    data = resp.get("data", resp)
    name = data.get("file_name") or data.get("filename") or "Untitled"
    rid = data.get("file_id") or data.get("id") or "?"
    print(f"Recording: {name}")
    print(f"ID:        {rid}")
    print(f"Date:      {ms_to_datetime(data.get('start_time', 0))}")
    print(f"Duration:  {ms_to_duration(data.get('duration', 0))}")
    print(f"Device:    {data.get('serial_number', 'Unknown')}")
    print()

    content_list = data.get("content_list", [])

    trans = _extract_transcript_sync(content_list)
    if trans:
        print("=== TRANSCRIPT ===")
        print(trans)
        print()
    else:
        print("(No transcript available)")
        print()

    summary = _extract_summary_sync(content_list)
    if summary:
        print("=== AI SUMMARY ===")
        print(summary)
    else:
        print("(No AI summary available)")


def cmd_search(client: PlaudClient, args: argparse.Namespace) -> None:
    resp = client.list_recordings()
    recordings = resp.get("data_file_list", [])

    query = args.query.lower()
    matches = [
        r for r in recordings
        if query in (r.get("filename") or "").lower()
        or query in " ".join(r.get("keywords") or []).lower()
    ]

    if args.json:
        print(json.dumps(matches, indent=2, ensure_ascii=False))
        return

    if not matches:
        print(f"No recordings matching '{args.query}'")
        return

    print(f"Found {len(matches)} recording(s) matching '{args.query}':\n")
    print_recordings_table(matches)


def cmd_export(client: PlaudClient, args: argparse.Namespace) -> None:
    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    resp = client.list_recordings()
    recordings = resp.get("data_file_list", [])

    exported = 0
    skipped = 0

    for r in recordings:
        file_id = r.get("id")
        name = (r.get("filename") or file_id or "untitled").replace("/", "_")
        has_trans = r.get("is_trans", False)
        has_summary = r.get("is_summary", False)

        if not has_trans and not has_summary:
            skipped += 1
            continue

        print(f"Fetching: {name}...", end=" ", flush=True)

        try:
            detail = client.get_detail(file_id)
            data = detail.get("data", detail)
        except SystemExit as e:
            print(f"ERROR: {e}")
            continue

        # Safe filename
        safe_name = "".join(c if c.isalnum() or c in " -_." else "_" for c in name)

        content_list = data.get("content_list", [])
        trans = _extract_transcript_sync(content_list)
        summary = _extract_summary_sync(content_list)

        if args.format == "json":
            out_path = output_dir / f"{safe_name}.json"
            with open(out_path, "w") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        elif args.format == "md":
            out_path = output_dir / f"{safe_name}.md"
            with open(out_path, "w") as f:
                f.write(f"# {name}\n\n")
                f.write(f"**Date:** {ms_to_datetime(data.get('start_time', 0))}\n")
                f.write(f"**Duration:** {ms_to_duration(data.get('duration', 0))}\n\n")
                if trans:
                    f.write(f"## Transcript\n\n{trans}\n\n")
                if summary:
                    f.write(f"## AI Summary\n\n{summary}\n")
        else:  # txt
            out_path = output_dir / f"{safe_name}.txt"
            with open(out_path, "w") as f:
                f.write(f"{name}\n")
                f.write(f"Date: {ms_to_datetime(data.get('start_time', 0))}\n")
                f.write(f"Duration: {ms_to_duration(data.get('duration', 0))}\n\n")
                if trans:
                    f.write(f"TRANSCRIPT:\n{trans}\n\n")
                if summary:
                    f.write(f"AI SUMMARY:\n{summary}\n")

        exported += 1
        print("OK")

    print(f"\nExported {exported} recording(s) to {output_dir}/")
    if skipped:
        print(f"Skipped {skipped} recording(s) with no transcript or summary.")


def cmd_devices(client: PlaudClient, args: argparse.Namespace) -> None:
    resp = client.list_devices()

    if args.json:
        print(json.dumps(resp, indent=2, ensure_ascii=False))
        return

    devices = resp.get("data_devices", [])
    if not devices:
        print("No devices found.")
        return

    print(f"Found {len(devices)} device(s):\n")
    for d in devices:
        print(f"  {d.get('name', 'Unknown')} (SN: {d.get('sn', '?')}, Model: {d.get('model', '?')})")


def cmd_test(client: PlaudClient, _args: argparse.Namespace) -> None:
    print(f"API Domain: {client.api_domain}")
    print(f"Token (first 20 chars): {client.token[:20]}...")
    print(f"Token length: {len(client.token)}")
    print("Testing connection to Plaud API...", end=" ", flush=True)
    if client.test_connection():
        print("SUCCESS — token is valid.")
    else:
        print("FAILED — see error above.")
        sys.exit(1)


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Plaud API Client — pull transcripts and summaries from your Plaud recordings"
    )
    parser.add_argument("--env", help="Path to .env file", default=None)

    sub = parser.add_subparsers(dest="command", help="Available commands")

    # list
    p_list = sub.add_parser("list", help="List all recordings")
    p_list.add_argument("--json", action="store_true", help="Output raw JSON")
    p_list.add_argument("--limit", type=int, default=99999, help="Max recordings")

    # details
    p_detail = sub.add_parser("details", help="Get recording details with transcript")
    p_detail.add_argument("file_id", help="Recording file ID")
    p_detail.add_argument("--json", action="store_true", help="Output raw JSON")

    # search
    p_search = sub.add_parser("search", help="Search recordings by keyword")
    p_search.add_argument("query", help="Search term")
    p_search.add_argument("--json", action="store_true", help="Output raw JSON")

    # export
    p_export = sub.add_parser("export", help="Export all transcripts to files")
    p_export.add_argument("--output-dir", default="./plaud-exports", help="Output directory")
    p_export.add_argument("--format", choices=["txt", "md", "json"], default="txt",
                          help="Export format (default: txt)")

    # devices
    p_devices = sub.add_parser("devices", help="List connected devices")
    p_devices.add_argument("--json", action="store_true", help="Output raw JSON")

    # test
    sub.add_parser("test", help="Test API connection")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    # Load config
    env = load_env(args.env)
    token = env.get("PLAUD_TOKEN") or os.environ.get("PLAUD_TOKEN")
    api_domain = (env.get("PLAUD_API_DOMAIN") or os.environ.get("PLAUD_API_DOMAIN")
                  or DEFAULT_API_DOMAIN)

    if not token:
        print("ERROR: No PLAUD_TOKEN found.", file=sys.stderr)
        print("Create ~/.config/plaud/.env with:", file=sys.stderr)
        print("  PLAUD_TOKEN=bearer eyJ...your_token_here", file=sys.stderr)
        print("  PLAUD_API_DOMAIN=https://api.plaud.ai", file=sys.stderr)
        print("\nSee README.md for token extraction instructions.", file=sys.stderr)
        sys.exit(1)

    client = PlaudClient(token, api_domain)

    commands = {
        "list": cmd_list,
        "details": cmd_details,
        "search": cmd_search,
        "export": cmd_export,
        "devices": cmd_devices,
        "test": cmd_test,
    }
    commands[args.command](client, args)


if __name__ == "__main__":
    main()
