#!/usr/bin/env python3
"""Pyvorin Edge Inspect CLI.

Inspect bundle manifests, ABI contracts, configs, SQLite databases,
sync queues, and privacy audit chains.
"""

import argparse
import json
import sqlite3
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional

try:
    import tomllib

    _HAS_TOML = True
except Exception:  # pragma: no cover
    try:
        import tomli as tomllib  # type: ignore[no-redef]

        _HAS_TOML = True
    except Exception:
        _HAS_TOML = False


def _print_json(data: Any) -> None:
    print(json.dumps(data, indent=2))


def _print_table(headers: List[str], rows: List[List[str]]) -> None:
    if not rows:
        print("No data.")
        return
    col_widths = [
        max(len(str(row[i])) for row in [headers] + rows) for i in range(len(headers))
    ]
    sep = "+".join("-" * (w + 2) for w in col_widths)

    def fmt(row: List[str]) -> str:
        return "|".join(
            f" {str(row[i]).ljust(col_widths[i])} " for i in range(len(row))
        )

    print(f"+{sep}+")
    print(f"|{fmt(headers)}|")
    print(f"+{sep}+")
    for row in rows:
        print(f"|{fmt(row)}|")
    print(f"+{sep}+")


def _output(data: Any, as_json: bool) -> None:
    if as_json:
        _print_json(data)
        return
    if isinstance(data, list) and data and isinstance(data[0], dict):
        headers = list(data[0].keys())
        rows = [[str(item.get(h, "")) for h in headers] for item in data]
        _print_table(headers, rows)
    elif isinstance(data, dict):
        headers = ["Key", "Value"]
        rows = [[str(k), str(v)] for k, v in data.items()]
        _print_table(headers, rows)
    else:
        print(str(data))


def _require_path(path: str, must_exist: bool = True) -> Path:
    p = Path(path)
    if must_exist and not p.exists():
        raise FileNotFoundError(f"Path not found: {p}")
    return p


def _read_json(path: Path) -> Any:
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def cmd_manifest(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        data = _read_json(path)
        if not isinstance(data, dict):
            print("Manifest must be a JSON object", file=sys.stderr)
            return 1
        files = data.get("files")
        entry_count = len(files) if isinstance(files, dict) else 0
        if args.json:
            result = {
                "path": str(path),
                "keys": list(data.keys()),
                "entry_count": entry_count,
                "manifest": data,
            }
            _output(result, True)
        else:
            print(f"Manifest: {path}")
            print(f"Keys: {', '.join(data.keys())}")
            print(f"Files: {entry_count}")
            if isinstance(files, dict):
                rows = [[k, v] for k, v in files.items()]
                _print_table(["File", "Hash"], rows)
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def _nm_symbols(path: Path) -> List[Dict[str, str]]:
    try:
        out = subprocess.run(
            ["nm", "-D", str(path)],
            capture_output=True,
            text=True,
            check=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        return []
    symbols: List[Dict[str, str]] = []
    for line in out.stdout.splitlines():
        parts = line.strip().split()
        if len(parts) >= 3:
            address, sym_type, name = parts[0], parts[1], parts[2]
            symbols.append({"address": address, "type": sym_type, "name": name})
        elif len(parts) == 2:
            sym_type, name = parts[0], parts[1]
            symbols.append({"address": "", "type": sym_type, "name": name})
    return symbols


def _readelf_symbols(path: Path) -> List[Dict[str, str]]:
    try:
        out = subprocess.run(
            ["readelf", "-s", str(path)],
            capture_output=True,
            text=True,
            check=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        return []
    symbols: List[Dict[str, str]] = []
    for line in out.stdout.splitlines():
        if "FUNC" in line or "OBJECT" in line:
            parts = line.strip().split()
            if len(parts) >= 8:
                name = parts[-1]
                sym_type = "FUNC" if "FUNC" in line else "OBJECT"
                symbols.append({"address": parts[1], "type": sym_type, "name": name})
    return symbols


def cmd_abi(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        symbols = _nm_symbols(path)
        if not symbols:
            symbols = _readelf_symbols(path)
        if not symbols:
            print(
                "Could not extract ABI symbols (nm/readelf not available or no symbols)",
                file=sys.stderr,
            )
            return 1
        if args.json:
            result = {
                "path": str(path),
                "symbol_count": len(symbols),
                "symbols": symbols,
            }
            _output(result, True)
        else:
            print(f"ABI: {path} ({len(symbols)} symbols)")
            rows = [[s["address"], s["type"], s["name"]] for s in symbols]
            _print_table(["Address", "Type", "Name"], rows)
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def _flatten_dict(d: Dict[str, Any], parent: str = "") -> Dict[str, Any]:
    items: Dict[str, Any] = {}
    for k, v in d.items():
        key = f"{parent}.{k}" if parent else k
        if isinstance(v, dict):
            items.update(_flatten_dict(v, key))
        else:
            items[key] = v
    return items


def cmd_config(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        if _HAS_TOML:
            with open(path, "rb") as f:
                data = tomllib.load(f)
        else:
            data = _read_json(path)
        flattened = _flatten_dict(data)
        if args.json:
            result = {
                "path": str(path),
                "keys": list(flattened.keys()),
                "config": flattened,
            }
            _output(result, True)
        else:
            print(f"Config: {path}")
            rows = [[k, str(v)] for k, v in flattened.items()]
            _print_table(["Key", "Value"], rows)
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def _sqlite_tables(conn: sqlite3.Connection) -> List[str]:
    cursor = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    return [row[0] for row in cursor.fetchall()]


def _sqlite_schema(conn: sqlite3.Connection, table: str) -> List[Dict[str, Any]]:
    cursor = conn.execute(f'PRAGMA table_info("{table}")')
    cols: List[Dict[str, Any]] = []
    for row in cursor.fetchall():
        cols.append(
            {
                "cid": row[0],
                "name": row[1],
                "type": row[2],
                "notnull": row[3],
                "default": row[4],
                "pk": row[5],
            }
        )
    return cols


def _sqlite_row_count(conn: sqlite3.Connection, table: str) -> int:
    cursor = conn.execute(f'SELECT COUNT(*) FROM "{table}"')
    return cursor.fetchone()[0]


def _sqlite_sample(
    conn: sqlite3.Connection, table: str, limit: int = 5
) -> List[Dict[str, Any]]:
    cursor = conn.execute(f'SELECT * FROM "{table}" LIMIT {limit}')
    rows = cursor.fetchall()
    columns = [desc[0] for desc in cursor.description]
    return [dict(zip(columns, row)) for row in rows]


def cmd_db(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        conn = sqlite3.connect(str(path))
        try:
            tables = _sqlite_tables(conn)
            if args.table:
                if args.table not in tables:
                    print(f"Table not found: {args.table}", file=sys.stderr)
                    return 1
                result = {
                    "path": str(path),
                    "table": args.table,
                    "schema": _sqlite_schema(conn, args.table),
                    "row_count": _sqlite_row_count(conn, args.table),
                    "sample": _sqlite_sample(conn, args.table, 5),
                }
            else:
                table_info = []
                for t in tables:
                    table_info.append(
                        {
                            "name": t,
                            "row_count": _sqlite_row_count(conn, t),
                        }
                    )
                result = {
                    "path": str(path),
                    "tables": table_info,
                }
            _output(result, args.json)
        finally:
            conn.close()
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def cmd_queue(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        conn = sqlite3.connect(str(path))
        try:
            tables = _sqlite_tables(conn)
            queue_tables = [
                t for t in tables if "queue" in t.lower() or "sync" in t.lower()
            ]
            if not queue_tables:
                queue_tables = tables
            result: Dict[str, Any] = {"path": str(path), "queues": []}
            for t in queue_tables:
                info: Dict[str, Any] = {
                    "table": t,
                    "row_count": _sqlite_row_count(conn, t),
                }
                try:
                    cursor = conn.execute(
                        f'SELECT status, COUNT(*) FROM "{t}" GROUP BY status'
                    )
                    status_counts = {row[0]: row[1] for row in cursor.fetchall()}
                    info["status_counts"] = status_counts
                except Exception:
                    info["status_counts"] = {}
                result["queues"].append(info)
            _output(result, args.json)
        finally:
            conn.close()
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def cmd_audit(args: argparse.Namespace) -> int:
    try:
        path = _require_path(args.path)
        conn = sqlite3.connect(str(path))
        try:
            tables = _sqlite_tables(conn)
            audit_tables = [t for t in tables if "audit" in t.lower()]
            if not audit_tables:
                audit_tables = tables
            result: Dict[str, Any] = {"path": str(path), "audit_chains": []}
            for t in audit_tables:
                info: Dict[str, Any] = {
                    "table": t,
                    "row_count": _sqlite_row_count(conn, t),
                }
                try:
                    info["latest"] = _sqlite_sample(conn, t, 5)
                except Exception:
                    info["latest"] = []
                result["audit_chains"].append(info)
            _output(result, args.json)
        finally:
            conn.close()
        return 0
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


def main(argv: Optional[List[str]] = None) -> int:
    parser = argparse.ArgumentParser(
        prog="pyv-edge-inspect",
        description="Pyvorin Edge Inspect CLI",
    )
    parser.add_argument("--json", action="store_true", help="Output raw JSON")
    sub = parser.add_subparsers(dest="command", required=True)

    p_manifest = sub.add_parser("manifest", help="Inspect a bundle manifest")
    p_manifest.add_argument("path", help="Path to manifest JSON")
    p_manifest.set_defaults(func=cmd_manifest)

    p_abi = sub.add_parser("abi", help="Show ABI contract from .so")
    p_abi.add_argument("path", help="Path to shared object")
    p_abi.set_defaults(func=cmd_abi)

    p_config = sub.add_parser("config", help="Validate and show config")
    p_config.add_argument("path", help="Path to config file (TOML/JSON)")
    p_config.set_defaults(func=cmd_config)

    p_db = sub.add_parser("db", help="Inspect SQLite database")
    p_db.add_argument("path", help="Path to SQLite database")
    p_db.add_argument("--table", default=None, help="Specific table to inspect")
    p_db.set_defaults(func=cmd_db)

    p_queue = sub.add_parser("queue", help="Show cloud sync queue stats")
    p_queue.add_argument("path", help="Path to SQLite database")
    p_queue.set_defaults(func=cmd_queue)

    p_audit = sub.add_parser("audit", help="Show privacy audit chain")
    p_audit.add_argument("path", help="Path to SQLite database")
    p_audit.set_defaults(func=cmd_audit)

    args = parser.parse_args(argv)
    return args.func(args)


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