#!/usr/bin/env python3
"""cccfg - Claude Code config explorer (TUI).

A keyboard-driven inventory of every Claude Code setting:
- environment variables (extracted from the binary)
- settings.json keys
- CLI flags
- slash commands
- hook events

Usage:
  cccfg                    # launch TUI
  cccfg --list             # print full catalog to stdout
  cccfg --json             # print catalog as JSON
  cccfg --search QUERY     # search and print matching entries
  cccfg --category CAT     # filter by category
  cccfg --kind env|settings|cli|slash|event

TUI keys:
  j / k      navigate down / up in entry list
  / or s     focus search
  c          focus category list
  Enter      open details panel
  e          jump to environment value (current setting)
  ?          help / cookbook
  q          quit
"""
from __future__ import annotations
import os
import sys
import json
import argparse
from pathlib import Path

# Allow running from anywhere
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPT_DIR))

from catalog import get_all_entries, CATEGORIES  # noqa: E402

ENTRIES = get_all_entries()


def current_value(entry):
    """Look up the current value for an entry, if applicable."""
    kind = entry["kind"]
    if kind == "env":
        return os.environ.get(entry["id"])
    if kind == "settings":
        # Try ~/.claude/settings.json
        settings_path = Path.home() / ".claude" / "settings.json"
        if settings_path.exists():
            try:
                with open(settings_path) as f:
                    settings = json.load(f)
                # Look up nested via dot path
                node = settings
                for key in entry["id"].split("."):
                    if isinstance(node, dict) and key in node:
                        node = node[key]
                    else:
                        return None
                # Render compactly
                if isinstance(node, (dict, list)):
                    return json.dumps(node, indent=2)[:400]
                return str(node)
            except Exception:
                return None
    return None


def filter_entries(entries, search="", category="", kind=""):
    """Filter entries by free-text search, category, and kind."""
    q = search.lower()
    result = []
    for e in entries:
        if category and e.get("category") != category:
            continue
        if kind and e.get("kind") != kind:
            continue
        if q:
            blob = " ".join([
                e.get("id", ""),
                e.get("summary", ""),
                e.get("description", ""),
                e.get("usecase", ""),
                e.get("category", ""),
            ]).lower()
            if q not in blob:
                continue
        result.append(e)
    return result


# ============================================================
# Command-line non-TUI modes (--list, --json, --search, etc.)
# ============================================================
def cli_list(entries):
    by_cat = {}
    for e in entries:
        by_cat.setdefault(e["category"], []).append(e)
    for cat in CATEGORIES:
        items = by_cat.get(cat, [])
        if not items:
            continue
        print(f"\n=== {cat} ({len(items)}) ===")
        for e in sorted(items, key=lambda x: x["id"]):
            tag = e["kind"][:3]
            sum_ = e.get("summary", "")
            print(f"  [{tag}] {e['id']:<48} {sum_}")


def cli_search(entries, term):
    matches = filter_entries(entries, search=term)
    if not matches:
        print(f"No matches for {term!r}.")
        return
    for e in matches[:50]:
        print(f"\n=== {e['id']} ({e['category']}, {e['kind']}) ===")
        print(f"  {e.get('summary','')}")
        if e.get("description"):
            print(f"  {e['description']}")
        if e.get("usecase"):
            print(f"  Use case: {e['usecase']}")
        if e.get("example"):
            print(f"  Example: {e['example']}")
        cv = current_value(e)
        if cv is not None:
            print(f"  CURRENT: {cv[:200]}")
    if len(matches) > 50:
        print(f"\n... and {len(matches) - 50} more")


def cli_json(entries):
    print(json.dumps(entries, indent=2))


# ============================================================
# TUI
# ============================================================
def run_tui(entries):
    from textual.app import App, ComposeResult
    from textual.widgets import Header, Footer, Static, Input, Tree, ListView, ListItem, Label, Markdown
    from textual.containers import Horizontal, Vertical, VerticalScroll
    from textual.reactive import reactive
    from textual.binding import Binding

    class CategoryItem(ListItem):
        def __init__(self, cat_name, count):
            super().__init__(Label(f"  {cat_name}  ({count})", markup=False))
            self.cat_name = cat_name

    class EntryItem(ListItem):
        def __init__(self, entry):
            tag = {"env": "ENV", "settings": "SET", "cli": "CLI",
                   "slash": "SLA", "event": "EVT"}.get(entry["kind"], "???")
            doc = "*" if entry.get("docs") else " "
            title = f"  {tag}  {doc} {entry['id']}"
            super().__init__(Label(title, markup=False))
            self.entry = entry

    class CCCfgApp(App):
        CSS = """
        Screen { layout: horizontal; }

        #left {
            width: 24;
            min-width: 18;
            border-right: solid #0a7f7a;
        }
        #middle {
            width: 36;
            min-width: 30;
            border-right: solid #2c3036;
        }
        #right {
            background: #1a1d22;
            padding: 1 2;
            min-width: 30;
        }

        Header { background: #0a7f7a; color: white; }
        Footer { background: #0a7f7a; }

        #search {
            border: solid #0a7f7a;
            margin: 0;
        }
        #search:focus { border: solid #e2725b; }

        ListView { background: #14181d; }
        ListView > ListItem { padding: 0 1; }
        ListView > ListItem.--highlight {
            background: #0a7f7a;
            color: white;
        }

        #detail-title {
            color: #e2725b;
            text-style: bold;
            margin-bottom: 1;
        }
        #detail-meta { color: #9099a3; margin-bottom: 1; }
        #detail-summary { color: white; margin-bottom: 1; }
        #detail-desc { color: #c8ccd2; margin-bottom: 1; }
        #detail-usecase {
            color: #6dd4cf;
            margin: 1 0;
            padding: 0 1;
            border-left: thick #6dd4cf;
        }
        #detail-example {
            color: #f3a48c;
            margin: 1 0;
        }
        #detail-current {
            color: #6dd4a0;
            margin: 1 0;
            padding: 0 1;
            border-left: thick #6dd4a0;
        }
        #right-hint {
            color: #5a6068;
            margin-top: 1;
            text-style: italic;
        }
        """

        BINDINGS = [
            Binding("q", "quit", "Quit"),
            Binding("slash", "focus_search", "Search"),
            Binding("c", "focus_categories", "Categories"),
            Binding("enter", "open_manual", "Manual page", show=True),
            Binding("m", "open_manual", "Manual page", show=True),
            Binding("?", "show_help", "Cookbook"),
            Binding("escape", "focus_entries", "Entries"),
            Binding("k", "cursor_up", "Up", show=False),
            Binding("j", "cursor_down", "Down", show=False),
        ]

        current_search = reactive("")
        current_category = reactive("")
        selected_entry = reactive(None)

        def compose(self) -> ComposeResult:
            yield Header(show_clock=False)
            with Horizontal():
                # Left: categories
                with Vertical(id="left"):
                    yield Static("[b]Categories[/b]", id="cat-title")
                    yield ListView(id="categories")
                # Middle: entries
                with Vertical(id="middle"):
                    yield Input(placeholder="Search (j/k navigate, / search, ? help)", id="search")
                    yield ListView(id="entries")
                # Right: detail
                with VerticalScroll(id="right"):
                    yield Static("", id="detail-title")
                    yield Static("", id="detail-meta")
                    yield Static("", id="detail-summary")
                    yield Static("", id="detail-desc")
                    yield Static("", id="detail-usecase")
                    yield Static("", id="detail-example")
                    yield Static("", id="detail-current")
                    yield Static("", id="detail-defaults")
                    yield Static("Press [b]Enter[/b] or [b]m[/b] to open full manual page", id="right-hint")
            yield Footer()

        def on_mount(self):
            self.title = "cccfg - Claude Code config explorer"
            self.sub_title = f"{len(ENTRIES)} entries"
            self.populate_categories()
            self.populate_entries()

        def populate_categories(self):
            cats = self.query_one("#categories", ListView)
            cats.clear()
            counts = {}
            for e in ENTRIES:
                counts[e["category"]] = counts.get(e["category"], 0) + 1
            cats.append(CategoryItem("All", len(ENTRIES)))
            for cat in CATEGORIES:
                if counts.get(cat):
                    cats.append(CategoryItem(cat, counts[cat]))
            # Categories that aren't in CATEGORIES (just in case)
            for cat, count in counts.items():
                if cat not in CATEGORIES:
                    cats.append(CategoryItem(cat, count))

        def populate_entries(self):
            entries_view = self.query_one("#entries", ListView)
            entries_view.clear()
            cat = self.current_category if self.current_category != "All" else ""
            filtered = filter_entries(ENTRIES, search=self.current_search, category=cat)
            # Sort: documented first, then alpha
            filtered.sort(key=lambda x: (0 if x.get("docs") else 1, x["id"]))
            for e in filtered[:500]:  # safety cap
                entries_view.append(EntryItem(e))
            self.sub_title = f"{len(filtered)} matching entries"

        def on_list_view_highlighted(self, event):
            if event.list_view.id == "entries":
                item = event.item
                if isinstance(item, EntryItem):
                    self.selected_entry = item.entry
                    self.render_detail(item.entry)
            elif event.list_view.id == "categories":
                item = event.item
                if isinstance(item, CategoryItem):
                    self.current_category = item.cat_name
                    self.populate_entries()

        def on_input_changed(self, event):
            if event.input.id == "search":
                self.current_search = event.value
                self.populate_entries()

        def render_detail(self, entry):
            kind_label = {
                "env": "Environment variable",
                "settings": "settings.json key",
                "cli": "CLI flag",
                "slash": "Slash command",
                "event": "Hook event",
            }.get(entry["kind"], entry["kind"])
            documented = "Documented" if entry.get("docs") else "Discovered"

            self.query_one("#detail-title", Static).update(f"[b]{entry['id']}[/b]")
            self.query_one("#detail-meta", Static).update(
                f"[#9099a3]{kind_label}  |  {entry.get('category','')}  |  {documented}[/#9099a3]"
            )
            self.query_one("#detail-summary", Static).update(entry.get("summary", ""))
            self.query_one("#detail-desc", Static).update(entry.get("description", ""))
            uc = entry.get("usecase", "")
            if uc:
                self.query_one("#detail-usecase", Static).update(f"[b]Use when:[/b] {uc}")
            else:
                self.query_one("#detail-usecase", Static).update("")
            ex = entry.get("example", "")
            if ex:
                self.query_one("#detail-example", Static).update(f"[b]Example:[/b] {ex}")
            else:
                self.query_one("#detail-example", Static).update("")
            cv = current_value(entry)
            if cv is not None:
                self.query_one("#detail-current", Static).update(f"[b]Currently:[/b] {cv}")
            else:
                self.query_one("#detail-current", Static).update("[#5a6068]No current value set[/#5a6068]")
            df = entry.get("default", "")
            if df:
                self.query_one("#detail-defaults", Static).update(f"[b]Default:[/b] {df}")
            else:
                self.query_one("#detail-defaults", Static).update("")

        def action_focus_search(self):
            self.query_one("#search", Input).focus()

        def action_focus_categories(self):
            self.query_one("#categories", ListView).focus()

        def action_focus_entries(self):
            self.query_one("#entries", ListView).focus()

        def action_cursor_up(self):
            focused = self.focused
            if focused and hasattr(focused, "action_cursor_up"):
                focused.action_cursor_up()

        def action_cursor_down(self):
            focused = self.focused
            if focused and hasattr(focused, "action_cursor_down"):
                focused.action_cursor_down()

        def action_show_help(self):
            self.push_screen(HelpScreen())

        def action_open_manual(self):
            if self.selected_entry:
                self.push_screen(ManualScreen(self.selected_entry))

    class HelpScreen(App.ScreenType if hasattr(App, 'ScreenType') else object):
        pass

    # Simplified: use Textual's standard Screen API
    from textual.screen import Screen

    class HelpScreen(Screen):
        CSS = """
        Screen { background: #14181d; align: center middle; padding: 2; }
        #help-content { width: 80%; height: 80%; background: #1a1d22; border: round #0a7f7a; padding: 2; }
        """
        BINDINGS = [
            Binding("q", "app.pop_screen", "Close"),
            Binding("escape", "app.pop_screen", "Close"),
        ]

        def compose(self):
            yield VerticalScroll(Markdown(HELP_TEXT), id="help-content")

    class ManualScreen(Screen):
        """Full-screen detailed view of a single config entry. The 'man page' for a setting."""
        CSS = """
        Screen { background: #0f1216; align: center middle; padding: 2; }
        #manual-frame {
            width: 90%; height: 90%;
            background: #14181d;
            border: round #0a7f7a;
            padding: 1 3;
        }
        #manual-content { padding: 0 1; }
        """
        BINDINGS = [
            Binding("q", "app.pop_screen", "Close"),
            Binding("escape", "app.pop_screen", "Close"),
        ]

        def __init__(self, entry):
            super().__init__()
            self.entry = entry

        def compose(self):
            md = self._render_manual(self.entry)
            with VerticalScroll(id="manual-frame"):
                yield Markdown(md, id="manual-content")

        def _render_manual(self, e):
            kind_label = {
                "env": "Environment variable",
                "settings": "settings.json key",
                "cli": "CLI flag",
                "slash": "Slash command",
                "event": "Hook event",
            }.get(e["kind"], e["kind"])
            documented = "Officially documented" if e.get("docs") else "Discovered (auto-extracted from binary)"

            md = f"# {e['id']}\n\n"
            md += f"**{kind_label}** · {e.get('category','')} · {documented}\n\n"
            md += "---\n\n"

            if e.get("summary"):
                md += f"## Summary\n\n{e['summary']}\n\n"
            if e.get("description"):
                md += f"## Description\n\n{e['description']}\n\n"
            if e.get("usecase"):
                md += f"## Use case\n\n{e['usecase']}\n\n"
            if e.get("example"):
                md += f"## Example\n\n```\n{e['example']}\n```\n\n"
            if e.get("default"):
                md += f"## Default\n\n`{e['default']}`\n\n"

            cv = current_value(e)
            if cv is not None:
                md += f"## Current value on this machine\n\n```\n{cv}\n```\n\n"
            else:
                md += "## Current value on this machine\n\nNot set.\n\n"

            # How to set it
            md += "## How to set\n\n"
            if e["kind"] == "env":
                md += f"In your shell:\n\n```\nexport {e['id']}=value\n```\n\n"
                md += f"Or in settings.json:\n\n```json\n{{\n  \"env\": {{\n    \"{e['id']}\": \"value\"\n  }}\n}}\n```\n\n"
            elif e["kind"] == "settings":
                md += f"In `~/.claude/settings.json` (or `./.claude/settings.json` for project-scope):\n\n"
                md += f"```json\n{{\n  \"{e['id']}\": ...\n}}\n```\n\n"
            elif e["kind"] == "cli":
                md += f"On the command line:\n\n```\nclaude {e['id']} ...\n```\n\n"
            elif e["kind"] == "slash":
                md += f"Inside Claude Code's interactive session:\n\n```\n{e['id']}\n```\n\n"
            elif e["kind"] == "event":
                md += f"Register in `settings.json` under hooks:\n\n```json\n{{\n  \"hooks\": {{\n    \"{e['id']}\": [\n      {{\n        \"matcher\": \"\",\n        \"hooks\": [{{ \"type\": \"command\", \"command\": \"./your-script.sh\" }}]\n      }}\n    ]\n  }}\n}}\n```\n\n"

            md += "---\n\n"
            md += "_Press **Esc** or **q** to return._\n"
            return md

    HELP_TEXT = """
# cccfg - Claude Code config explorer

## Keys

- **j / k** - navigate entries down / up
- **/** or **s** - focus search
- **c** - focus category list
- **Enter** - open entry details (sidebar)
- **Escape** - return to entries list
- **?** - this help
- **q** - quit

## Tags

- **[ENV]** - environment variable
- **[SET]** - settings.json key
- **[CLI]** - command-line flag
- **[/__]** - slash command
- **[EVT]** - hook event
- **\\*** marker - officially documented

## Cookbook

### "I want to put everything I use in a config file, not env vars"

Most CLAUDE_CODE_* env vars can be set via the `env` block in settings.json:

```json
{
  "env": {
    "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS": "1",
    "CLAUDE_CODE_EFFORT_LEVEL": "high"
  }
}
```

### "I want to lock down my team's Claude Code install"

In managed settings, set:

- `allowManagedHooksOnly`: true
- `allowManagedMcpServersOnly`: true
- `allowManagedPermissionRulesOnly`: true
- `disableBypassPermissionsMode`: true
- `strictPluginOnlyCustomization`: ["skills", "hooks"]

### "I want to use a secret vault for the API key"

Set `apiKeyHelper` to a script that prints the key:

```json
{
  "apiKeyHelper": "/usr/local/bin/get-anthropic-key.sh"
}
```

Tune `CLAUDE_CODE_API_KEY_HELPER_TTL_MS` for cache duration.

### "I want maximum privacy / no telemetry"

```
DISABLE_TELEMETRY=1
DISABLE_ERROR_REPORTING=1
DISABLE_GROWTHBOOK=1
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```

### "I want a CI-clean run with no harness baggage"

Use `--bare`. This skips hooks, LSP, plugins, attribution, auto-memory,
keychain reads, and CLAUDE.md auto-discovery. Anthropic auth becomes
strictly ANTHROPIC_API_KEY or apiKeyHelper.

### "I want to bypass auto-compaction"

```
DISABLE_AUTO_COMPACT=1
# or
DISABLE_COMPACT=1
```

Use /compact manually instead.

### "I want bigger Bash output and longer timeouts"

```
BASH_DEFAULT_TIMEOUT_MS=300000
BASH_MAX_TIMEOUT_MS=900000
BASH_MAX_OUTPUT_LENGTH=500000
```

### "I want to register a custom model in the picker"

```
ANTHROPIC_CUSTOM_MODEL_OPTION=claude-opus-4-7-tuned
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="Opus 4.7 (internal tuning)"
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="Internal mirror with custom tuning"
ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES="vision,tools,thinking"
```

### "I want to debug a hook"

```
--debug "hooks"
```

or set `CLAUDE_CODE_DEBUG_LOG_LEVEL=trace` and
`CLAUDE_CODE_DEBUG_LOGS_DIR=~/.claude/logs`.
"""

    app = CCCfgApp()
    app.run()


# ============================================================
# entrypoint
# ============================================================
def main():
    parser = argparse.ArgumentParser(
        description="cccfg - complete Claude Code config explorer")
    parser.add_argument("--list", action="store_true",
                        help="Print all entries grouped by category")
    parser.add_argument("--json", action="store_true",
                        help="Print catalog as JSON")
    parser.add_argument("--search", "-s", metavar="QUERY",
                        help="Search entries")
    parser.add_argument("--category", metavar="CAT",
                        help="Filter by category name")
    parser.add_argument("--kind", choices=["env", "settings", "cli", "slash", "event"],
                        help="Filter by entry kind")
    parser.add_argument("--cookbook", action="store_true",
                        help="Print the cookbook section and exit")
    args = parser.parse_args()

    entries = ENTRIES
    if args.category or args.kind:
        entries = filter_entries(entries, category=args.category or "", kind=args.kind or "")

    if args.cookbook:
        print(HELP_TEXT if 'HELP_TEXT' in globals() else "Cookbook lives in TUI ?")
        return

    if args.json:
        cli_json(entries)
    elif args.search:
        cli_search(entries, args.search)
    elif args.list:
        cli_list(entries)
    elif args.category or args.kind:
        # When filter flags are set without an output mode, default to --list
        cli_list(entries)
    else:
        run_tui(entries)


# Expose HELP_TEXT for --cookbook
HELP_TEXT = """
# cccfg - Claude Code config explorer

Cookbook (interactive TUI launches by default; this is the same content):

## "I want to put everything I use in a config file, not env vars"
Most CLAUDE_CODE_* env vars can be set via the `env` block in settings.json.

## "I want to lock down my team's Claude Code install"
In managed settings, set:
- allowManagedHooksOnly: true
- allowManagedMcpServersOnly: true
- allowManagedPermissionRulesOnly: true
- disableBypassPermissionsMode: true
- strictPluginOnlyCustomization: ["skills", "hooks"]

## "I want to use a secret vault for the API key"
Set `apiKeyHelper` to a script that prints the key on stdout.
Tune CLAUDE_CODE_API_KEY_HELPER_TTL_MS for cache duration.

## "I want maximum privacy / no telemetry"
DISABLE_TELEMETRY=1
DISABLE_ERROR_REPORTING=1
DISABLE_GROWTHBOOK=1
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

## "I want a CI-clean run with no harness baggage"
Use `--bare`. Skips hooks, LSP, plugins, attribution, auto-memory,
keychain, and CLAUDE.md auto-discovery. Auth is strictly
ANTHROPIC_API_KEY or apiKeyHelper.

## "I want to bypass auto-compaction"
DISABLE_AUTO_COMPACT=1  (or DISABLE_COMPACT=1)

## "I want bigger Bash output and longer timeouts"
BASH_DEFAULT_TIMEOUT_MS=300000
BASH_MAX_TIMEOUT_MS=900000
BASH_MAX_OUTPUT_LENGTH=500000

## "I want to register a custom model in the picker"
ANTHROPIC_CUSTOM_MODEL_OPTION=claude-opus-4-7-tuned
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="Opus 4.7 (internal)"
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="..."
ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES="vision,tools,thinking"

## "I want to debug a hook"
--debug "hooks"  (or CLAUDE_CODE_DEBUG_LOG_LEVEL=trace + CLAUDE_CODE_DEBUG_LOGS_DIR)
"""


if __name__ == "__main__":
    main()
