#!/usr/bin/env python3
"""openlibrary-cli — Open Library book metadata from the terminal.

Search books, authors, works, and lookup by ISBN using the public
Open Library API. No API key required.
"""

import argparse
import json
import os
import sys
import warnings
from typing import Any, Dict, List, Optional, Tuple

warnings.simplefilter("ignore")

import requests

# === Config ===
DEFAULT_SERVER = "https://openlibrary.org"
ENV_SERVER = os.getenv("OL_SERVER", DEFAULT_SERVER)
ENV_EMAIL = os.getenv("OL_EMAIL", "")
ENV_USER_AGENT = os.getenv("OL_USER_AGENT", "openlibrary-cli/1.0 (+https://github.com)")

QUIET = False
GLOBAL_FLAGS: Dict[str, Any] = {
    "json": False, "dry_run": False, "quiet": False, "verbose": False
}


def log(msg: str) -> None:
    if not QUIET and not GLOBAL_FLAGS.get("json", False):
        print(msg)


def warn(msg: str) -> None:
    print(f"Warning: {msg}", file=sys.stderr)


def die(msg: str, exit_code: int = 1) -> None:
    print(f"Error: {msg}", file=sys.stderr)
    sys.exit(exit_code)


def emit(human: str, data: Any) -> None:
    if GLOBAL_FLAGS.get("json", False):
        print(json.dumps(data, default=str))
    else:
        print(human)


def _preparse_global_flags(argv: List[str]) -> Tuple[Dict[str, Any], List[str]]:
    GLOBAL_BOOLS = {"--json", "--dry-run", "--quiet", "--verbose"}
    flags: Dict[str, Any] = {}
    filtered: List[str] = [argv[0]]
    i = 1
    while i < len(argv):
        arg = argv[i]
        if arg in GLOBAL_BOOLS:
            flags[arg.lstrip("-").replace("-", "_")] = True
            i += 1
        elif arg in ("--help", "-h"):
            return flags, argv
        elif arg == "--":
            filtered.extend(argv[i:])
            break
        else:
            filtered.append(arg)
            i += 1
    return flags, filtered


class OpenLibraryClient:
    """Client for the public Open Library API."""

    def __init__(self, server: str = "", dry_run: bool = False):
        self.server = (server or ENV_SERVER).rstrip("/")
        self.dry_run = dry_run

    def _headers(self) -> Dict[str, str]:
        h = {"User-Agent": ENV_USER_AGENT}
        if ENV_EMAIL:
            h["User-Agent"] += f" (mailto:{ENV_EMAIL})"
        return h

    def _get(self, path: str, params: Optional[Dict] = None) -> Any:
        url = f"{self.server}{path}"
        if self.dry_run:
            return {"dry_run": True, "url": url, "params": params}
        try:
            resp = requests.get(url, params=params, headers=self._headers(), timeout=30)
        except requests.ConnectionError as e:
            die(f"Cannot connect to {self.server}: {e}")
        if resp.status_code == 404:
            return None
        if resp.status_code >= 400:
            try:
                detail = resp.json()
            except Exception:
                detail = resp.text[:200]
            die(f"API error ({resp.status_code}): {detail}")
        try:
            return resp.json()
        except ValueError:
            return {"raw": resp.text[:500]}


def fmt_author(a: Dict) -> str:
    name = a.get("name", a.get("title", "?"))
    key = a.get("key", "").split("/")[-1] if a.get("key") else ""
    birth = a.get("birth_date", "")
    death = a.get("death_date", "")
    years = f" ({birth}–{death})" if birth or death else ""
    return f"  {name}{years}  [{key}]"


def cmd_search(client, args):
    parser = argparse.ArgumentParser(prog="openlibrary-cli search")
    parser.add_argument("--query", "-q", required=True)
    parser.add_argument("--limit", type=int, default=20)
    parser.add_argument("--offset", type=int, default=0)
    parser.add_argument("--sort", default="", choices=["", "editions", "new", "old", "rating", "title"])
    parser.add_argument("--lang", default="")
    parser.add_argument("--availability", default="")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would search: {parsed.query}", {"dry_run": True, "query": parsed.query})
        return

    params: Dict[str, Any] = {"q": parsed.query, "limit": parsed.limit, "offset": parsed.offset}
    if parsed.sort:
        params["sort"] = parsed.sort
    if parsed.lang:
        params["lang"] = parsed.lang
    data = client._get("/search.json", params) or {}

    docs = data.get("docs", [])
    if not docs:
        emit("No results.", {"results": []})
        return

    lines, out = [], []
    for d in docs:
        title = d.get("title", "?")
        authors = ", ".join(d.get("author_name", [])) or "?"
        year = d.get("first_publish_year", "")
        year_str = f" ({year})" if year else ""
        edition_count = d.get("edition_count", 0)
        lines.append(f"  {title:50}{year_str} — {authors}  ({edition_count} editions)")
        out.append({
            "title": title, "authors": d.get("author_name", []),
            "first_publish_year": year, "edition_count": edition_count,
            "key": d.get("key", ""), "isbn": d.get("isbn", [])[:3],
            "cover_edition": d.get("cover_edition_key", "")
        })

    total = data.get("numFound", len(docs))
    emit(f"{total} result(s):\n" + "\n".join(lines), {"total": total, "results": out})


def cmd_isbn(client, args):
    parser = argparse.ArgumentParser(prog="openlibrary-cli isbn")
    parser.add_argument("isbn", help="ISBN number")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would lookup ISBN: {parsed.isbn}", {"dry_run": True, "isbn": parsed.isbn})
        return

    data = client._get(f"/isbn/{parsed.isbn}.json")
    if not data:
        emit(f"ISBN {parsed.isbn} not found.", {"error": "not found", "isbn": parsed.isbn})
        return

    title = data.get("title", "?")
    authors_list = data.get("authors", [])
    author_names = ", ".join([a.get("name", a.get("key", "?")) for a in authors_list]) if authors_list else "?"
    pages = data.get("number_of_pages", data.get("pagination", "?"))
    publishers = ", ".join(data.get("publishers", [])) or "?"
    publish_date = data.get("publish_date", "?")
    subjects = ", ".join(data.get("subjects", [])[:5]) or "(none)"

    emit(
        f"📖 {title}\n"
        f"   Author(s): {author_names}\n"
        f"   Pages: {pages}  Published: {publish_date}\n"
        f"   Publisher: {publishers}\n"
        f"   Subjects: {subjects}",
        {"isbn": parsed.isbn, "title": title, "authors": author_names,
         "pages": pages, "publish_date": publish_date,
         "publishers": publishers, "subjects": data.get("subjects", [])}
    )


def cmd_author(client, args):
    parser = argparse.ArgumentParser(prog="openlibrary-cli author")
    parser.add_argument("key", help="Author key (e.g. OL23919A)")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would fetch author {parsed.key}", {"dry_run": True, "key": parsed.key})
        return

    data = client._get(f"/authors/{parsed.key}.json")
    if not data:
        emit(f"Author {parsed.key} not found.", {"error": "not found"})
        return

    name = data.get("name", "?")
    birth = data.get("birth_date", "")
    death = data.get("death_date", "")
    years = f" ({birth}–{death})" if birth or death else ""
    bio = data.get("bio", "")
    if isinstance(bio, dict):
        bio = bio.get("value", "")
    bio_short = f"\n{bio[:500]}" if bio else ""
    emit(
        f"👤 {name}{years}{bio_short}",
        {"key": parsed.key, "name": name, "birth_date": birth,
         "death_date": death, "bio": bio,
         "wikipedia": data.get("wikipedia", ""),
         "personal_name": data.get("personal_name", "")}
    )


def cmd_work(client, args):
    parser = argparse.ArgumentParser(prog="openlibrary-cli work")
    parser.add_argument("key", help="Work key (e.g. OL123W)")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would fetch work {parsed.key}", {"dry_run": True, "key": parsed.key})
        return

    data = client._get(f"/works/{parsed.key}.json")
    if not data:
        emit(f"Work {parsed.key} not found.", {"error": "not found"})
        return

    title = data.get("title", "?")
    authors = data.get("authors", [])
    author_str = ", ".join([a.get("author", {}).get("key", "?") for a in authors]) if authors else "?"
    desc = data.get("description", "")
    if isinstance(desc, dict):
        desc = desc.get("value", "")
    desc_short = f"\n{desc[:500]}" if desc else ""
    subjects = ", ".join(data.get("subjects", [])[:5]) or "(none)"

    emit(
        f"📖 {title}\n"
        f"   Author(s): {author_str}\n"
        f"   Subjects: {subjects}{desc_short}",
        {"key": parsed.key, "title": title, "authors": author_str,
         "description": desc, "subjects": data.get("subjects", [])}
    )


def cmd_search_authors(client, args):
    parser = argparse.ArgumentParser(prog="openlibrary-cli search-authors")
    parser.add_argument("--query", "-q", required=True)
    parser.add_argument("--limit", type=int, default=20)
    parser.add_argument("--offset", type=int, default=0)
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would search authors: {parsed.query}", {"dry_run": True, "query": parsed.query})
        return

    data = client._get("/search/authors.json", {"q": parsed.query, "limit": parsed.limit, "offset": parsed.offset}) or {}
    docs = data.get("docs", [])
    if not docs:
        emit("No authors found.", {"results": []})
        return

    lines, out = [], []
    for d in docs:
        name = d.get("name", "?")
        key = d.get("key", "").split("/")[-1] if d.get("key") else "?"
        birth = d.get("birth_date", "")
        death = d.get("death_date", "")
        years = f" ({birth}–{death})" if birth or death else ""
        top_work = d.get("top_work", "")
        work_str = f" — {top_work}" if top_work else ""
        lines.append(f"  {name:30}{years}  [{key}]{work_str}")
        out.append({"name": name, "key": key, "birth_date": birth, "death_date": death, "top_work": top_work})

    total = data.get("numFound", len(docs))
    emit(f"{total} author(s):\n" + "\n".join(lines), {"total": total, "results": out})


def main():
    global GLOBAL_FLAGS, QUIET
    GLOBAL_FLAGS, filtered_argv = _preparse_global_flags(sys.argv)
    if GLOBAL_FLAGS.get("quiet", False):
        QUIET = True
    if GLOBAL_FLAGS.get("json", False):
        warnings.simplefilter("ignore")

    parser = argparse.ArgumentParser(
        prog="openlibrary-cli",
        description="Open Library book metadata from the terminal. No API key required.",
        epilog="Global flags work anywhere: openlibrary-cli --json search --query 'dune'"
    )
    sub = parser.add_subparsers(dest="command")

    p_search = sub.add_parser("search", help="Search books")
    p_search.add_argument("--query", "-q", required=True)
    p_search.add_argument("--limit", type=int, default=20)
    p_search.add_argument("--offset", type=int, default=0)
    p_search.add_argument("--sort", default="", choices=["", "editions", "new", "old", "rating", "title"])
    p_search.add_argument("--lang")
    p_search.add_argument("--availability")

    p_sa = sub.add_parser("search-authors", help="Search authors")
    p_sa.add_argument("--query", "-q", required=True)
    p_sa.add_argument("--limit", type=int, default=20)
    p_sa.add_argument("--offset", type=int, default=0)

    sub.add_parser("author", help="Get author details") .add_argument("key")
    sub.add_parser("work", help="Get work details") .add_argument("key")
    sub.add_parser("isbn", help="Lookup by ISBN") .add_argument("isbn")

    args = parser.parse_args(filtered_argv[1:])
    if not args.command:
        parser.print_help()
        sys.exit(1)

    client = OpenLibraryClient(dry_run=GLOBAL_FLAGS.get("dry_run", False))

    cmd_map = {
        "search": cmd_search,
        "search-authors": cmd_search_authors,
        "author": cmd_author,
        "work": cmd_work,
        "isbn": cmd_isbn,
    }
    handler = cmd_map.get(args.command)
    if not handler:
        parser.print_help()
        sys.exit(1)

    remaining = filtered_argv[filtered_argv.index(args.command) + 1:]
    handler(client, remaining)


if __name__ == "__main__":
    main()
