#!/usr/bin/env python3
"""Odoo 12 CLI — Stateful CLI harness for Odoo 12 ERP via REST or XML-RPC.

Usage:
    # One-shot commands
    cli-anything-odoo_12 connect --url http://localhost:8069 --db test --user admin --password admin --save
    cli-anything-odoo_12 record search res.partner --domain "[('is_company','=',True)]" --limit 10
    cli-anything-odoo_12 module list --installed
    cli-anything-odoo_12 db list

    # Interactive REPL
    cli-anything-odoo_12
"""

import sys
import os
import json
import click
from typing import Optional

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from cli_anything.odoo_12.core import connection as conn_mod
from cli_anything.odoo_12.core import record as record_mod
from cli_anything.odoo_12.core import module as module_mod
from cli_anything.odoo_12.core import database as db_mod
from cli_anything.odoo_12.core import report as report_mod
from cli_anything.odoo_12.core import export as export_mod
from cli_anything.odoo_12.core import nlq as nlq_mod
from cli_anything.odoo_12.core import formatters as fmt_mod
from cli_anything.odoo_12.core import cross_group as cg_mod
from cli_anything.odoo_12.core.session import Session

_session: Optional[Session] = None
_json_output = False
_repl_mode = False


def get_session() -> Session:
    global _session
    if _session is None:
        from pathlib import Path
        sf = str(Path.home() / ".cli-anything-odoo_12" / "session.json")
        _session = Session(session_file=sf)
    return _session


def output(data, message: str = ""):
    if _json_output:
        click.echo(json.dumps(data, indent=2, default=str))
    else:
        if message:
            click.echo(message)
        if isinstance(data, dict):
            _print_dict(data)
        elif isinstance(data, list):
            _print_list(data)
        else:
            click.echo(str(data))


def _print_dict(d: dict, indent: int = 0):
    prefix = "  " * indent
    for k, v in d.items():
        if isinstance(v, dict):
            click.echo(f"{prefix}{k}:")
            _print_dict(v, indent + 1)
        elif isinstance(v, list):
            click.echo(f"{prefix}{k}:")
            _print_list(v, indent + 1)
        else:
            click.echo(f"{prefix}{k}: {v}")


def _print_list(items: list, indent: int = 0):
    prefix = "  " * indent
    for i, item in enumerate(items):
        if isinstance(item, dict):
            click.echo(f"{prefix}[{i}]")
            _print_dict(item, indent + 1)
        else:
            click.echo(f"{prefix}- {item}")


def handle_error(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except (FileNotFoundError, ValueError, RuntimeError, TimeoutError) as e:
            if _json_output:
                click.echo(json.dumps({"error": str(e), "type": type(e).__name__}))
            else:
                click.echo(f"Error: {e}", err=True)
            if not _repl_mode:
                sys.exit(1)
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper


def _get_rpc(url, db, user, password, backend=None):
    """Get an authenticated RPC client from CLI options."""
    return conn_mod.get_rpc(url=url, db=db, user=user, password=password, backend=backend)


# ── Main CLI Group ──────────────────────────────────────────────

@click.group(invoke_without_command=True)
@click.version_option(package_name="cli-anything-odoo_12", prog_name="cli-anything-odoo_12")
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
@click.option("--url", type=str, default=None, help="Odoo URL (http://host:port)")
@click.option("--db", type=str, default=None, help="Database name")
@click.option("--user", type=str, default=None, help="Username")
@click.option("--password", type=str, default=None, help="Password")
@click.option("--backend", type=click.Choice(["rest", "xmlrpc"]), default=None,
              help="Backend protocol (default: from config or 'rest')")
@click.pass_context
def cli(ctx, use_json, url, db, user, password, backend):
    """Odoo 12 CLI — Stateful CLI harness for Odoo 12 ERP."""
    global _json_output
    _json_output = use_json
    ctx.ensure_object(dict)
    ctx.obj["url"] = url
    ctx.obj["db"] = db
    ctx.obj["user"] = user
    ctx.obj["password"] = password
    ctx.obj["backend"] = backend

    if ctx.invoked_subcommand is None:
        ctx.invoke(repl)


# ── Connect Command Group ──────────────────────────────────────

@cli.command("connect")
@click.option("--url", type=str, default=None, help="Odoo URL")
@click.option("--db", type=str, default=None, help="Database name")
@click.option("--user", type=str, default=None, help="Username")
@click.option("--password", type=str, default=None, help="Password")
@click.option("--save", is_flag=True, help="Save connection to config")
@click.option("--backend", type=click.Choice(["rest", "xmlrpc"]), default=None,
              help="Backend protocol")
@click.option("--client-id", default=None, help="OAuth2 client ID (REST only)")
@click.option("--client-secret", default=None, help="OAuth2 client secret (REST only)")
@handle_error
def connect_cmd(url, db, user, password, save, backend, client_id, client_secret):
    """Connect to an Odoo instance and verify credentials."""
    result = conn_mod.connect(url=url, db=db, user=user, password=password,
                              save=save, backend=backend,
                              client_id=client_id, client_secret=client_secret)
    sess = get_session()
    sess.record("connect", {"url": url, "db": db, "user": user}, result)
    output(result, f"Connected to {result['url']} (uid={result['uid']})")


@cli.group("config", invoke_without_command=True)
@click.pass_context
def config_group(ctx):
    """Configuration management — show, delete, setup-rest."""
    if ctx.invoked_subcommand is None:
        result = conn_mod.show_config()
        output(result)


@config_group.command("show")
@handle_error
def config_show():
    """Show saved configuration."""
    result = conn_mod.show_config()
    output(result)


@config_group.command("delete")
@click.argument("key", required=False)
@handle_error
def config_delete(key):
    """Delete a config key or all config."""
    result = conn_mod.delete_config(key)
    output(result, f"Config {'key' if key else 'all'} deleted")


@config_group.command("setup-rest")
@click.option("--client-id", required=True, help="OAuth2 client ID")
@click.option("--client-secret", required=True, help="OAuth2 client secret")
@click.option("--url", default=None, help="Odoo URL")
@click.option("--db", default=None, help="Database name")
@click.option("--user", default=None, help="Username")
@click.option("--password", default=None, help="Password")
@handle_error
def config_setup_rest(client_id, client_secret, url, db, user, password):
    """Set up REST API backend with OAuth2 credentials."""
    result = conn_mod.setup_rest(
        client_id=client_id, client_secret=client_secret,
        url=url, db=db, user=user, password=password,
        grant_type="client_credentials",
    )
    output(result, f"REST API configured: {result['url']} (uid={result['uid']})")


# ── Record Command Group ───────────────────────────────────────

@cli.group()
def record():
    """Record CRUD — search, read, create, write, delete on any model."""
    pass


@record.command("search")
@click.argument("model")
@click.option("--domain", "-d", default="[]", help="Search domain")
@click.option("--fields", "-f", default="", help="Comma-separated fields")
@click.option("--limit", "-l", type=int, default=20, help="Max records")
@click.option("--offset", type=int, default=0, help="Offset")
@click.option("--order", default="", help="Sort order (e.g. 'name asc')")
@click.pass_context
@handle_error
def record_search(ctx, model, domain, fields, limit, offset, order):
    """Search records matching a domain."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.search(rpc, model, domain, fields, limit, offset, order)
    sess = get_session()
    sess.record("record search", {"model": model, "domain": domain}, {"count": result["count"]})
    output(result, f"Found {result['count']} record(s) in {model}")


@record.command("read")
@click.argument("model")
@click.argument("ids", type=str)
@click.option("--fields", "-f", default="", help="Comma-separated fields")
@click.pass_context
@handle_error
def record_read(ctx, model, ids, fields):
    """Read specific records by ID (comma-separated)."""
    id_list = [int(x.strip()) for x in ids.split(",")]
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.read(rpc, model, id_list, fields)
    output(result, f"Read {result['count']} record(s) from {model}")


@record.command("create")
@click.argument("model")
@click.option("--values", "-v", required=True, help="JSON values dict")
@click.pass_context
@handle_error
def record_create(ctx, model, values):
    """Create a new record."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.create(rpc, model, values)
    sess = get_session()
    sess.record("record create", {"model": model}, result)
    output(result, f"Created {model} record id={result['id']}")


@record.command("write")
@click.argument("model")
@click.argument("ids", type=str)
@click.option("--values", "-v", required=True, help="JSON values dict")
@click.pass_context
@handle_error
def record_write(ctx, model, ids, values):
    """Update existing records."""
    id_list = [int(x.strip()) for x in ids.split(",")]
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.write(rpc, model, id_list, values)
    sess = get_session()
    sess.record("record write", {"model": model, "ids": id_list}, result)
    output(result, f"Updated {len(id_list)} record(s) in {model}")


@record.command("unlink")
@click.argument("model")
@click.argument("ids", type=str)
@click.pass_context
@handle_error
def record_unlink(ctx, model, ids):
    """Delete records by ID (comma-separated)."""
    id_list = [int(x.strip()) for x in ids.split(",")]
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.unlink(rpc, model, id_list)
    sess = get_session()
    sess.record("record unlink", {"model": model, "ids": id_list}, result)
    output(result, f"Deleted {len(id_list)} record(s) from {model}")


@record.command("count")
@click.argument("model")
@click.option("--domain", "-d", default="[]", help="Search domain")
@click.pass_context
@handle_error
def record_count(ctx, model, domain):
    """Count records matching a domain."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.count(rpc, model, domain)
    output(result, f"{model}: {result['count']} record(s)")


@record.command("fields")
@click.argument("model")
@click.pass_context
@handle_error
def record_fields(ctx, model):
    """List fields of a model."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = record_mod.fields_get(rpc, model)
    output(result, f"{model}: {result['field_count']} fields")


# ── Module Command Group ───────────────────────────────────────

@cli.group()
def module():
    """Module management — list, info, install, update."""
    pass


@module.command("list")
@click.option("--installed", is_flag=True, help="Show only installed modules")
@click.option("--search", "-s", default=None, help="Search by name/description")
@click.pass_context
@handle_error
def module_list(ctx, installed, search):
    """List available modules."""
    state = "installed" if installed else None
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = module_mod.list_modules(rpc, state=state, search_term=search)
    output(result, f"Found {result['count']} module(s)")


@module.command("info")
@click.argument("name")
@click.pass_context
@handle_error
def module_info(ctx, name):
    """Get detailed info about a module."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = module_mod.module_info(rpc, name)
    output(result, f"Module: {result.get('name', name)}")


@module.command("install")
@click.argument("name")
@click.pass_context
@handle_error
def module_install(ctx, name):
    """Install a module."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = module_mod.install_module(rpc, name)
    sess = get_session()
    sess.record("module install", {"name": name}, result)
    output(result, f"Module '{name}' installed")


@module.command("update")
@click.argument("name")
@click.pass_context
@handle_error
def module_update(ctx, name):
    """Update a module."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = module_mod.update_module(rpc, name)
    sess = get_session()
    sess.record("module update", {"name": name}, result)
    output(result, f"Module '{name}' updated")


# ── Database Command Group ─────────────────────────────────────

@cli.group()
def db():
    """Database management — list, create, backup, restore, drop."""
    pass


@db.command("list")
@click.pass_context
@handle_error
def db_list(ctx):
    """List all databases."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = db_mod.list_databases(rpc)
    output(result, f"Found {result['count']} database(s)")


@db.command("create")
@click.argument("db_name")
@click.option("--admin-password", default="admin", help="Master password")
@click.option("--demo/--no-demo", default=False, help="Load demo data")
@click.option("--lang", default="pt_BR", help="Language")
@click.pass_context
@handle_error
def db_create(ctx, db_name, admin_password, demo, lang):
    """Create a new database."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = db_mod.create_database(rpc, db_name, admin_password, demo, lang)
    sess = get_session()
    sess.record("db create", {"db_name": db_name}, result)
    output(result, f"Database '{db_name}' created")


@db.command("drop")
@click.argument("db_name")
@click.option("--admin-password", default="admin", help="Master password")
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
@click.pass_context
@handle_error
def db_drop(ctx, db_name, admin_password, confirm):
    """Drop a database (DESTRUCTIVE)."""
    if not confirm and not _json_output:
        if not click.confirm(f"Really drop database '{db_name}'?"):
            click.echo("Aborted.")
            return
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = db_mod.drop_database(rpc, db_name, admin_password)
    sess = get_session()
    sess.record("db drop", {"db_name": db_name}, result)
    output(result, f"Database '{db_name}' dropped")


@db.command("backup")
@click.argument("db_name")
@click.argument("output_path")
@click.option("--admin-password", default="admin", help="Master password")
@click.option("--format", "backup_format", default="zip",
              type=click.Choice(["zip", "dump"]), help="Backup format")
@click.pass_context
@handle_error
def db_backup(ctx, db_name, output_path, admin_password, backup_format):
    """Backup a database to file."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = db_mod.backup_database(rpc, db_name, output_path, admin_password, backup_format)
    sess = get_session()
    sess.record("db backup", {"db_name": db_name}, result)
    output(result, f"Backup saved: {output_path} ({result['size']:,} bytes)")


@db.command("restore")
@click.argument("db_name")
@click.argument("backup_path")
@click.option("--admin-password", default="admin", help="Master password")
@click.pass_context
@handle_error
def db_restore(ctx, db_name, backup_path, admin_password):
    """Restore a database from backup."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = db_mod.restore_database(rpc, db_name, backup_path, admin_password)
    sess = get_session()
    sess.record("db restore", {"db_name": db_name}, result)
    output(result, f"Database '{db_name}' restored from {backup_path}")


# ── Report Command Group ───────────────────────────────────────

@cli.group()
def report():
    """Report generation — render and list reports."""
    pass


@report.command("render")
@click.argument("report_name")
@click.argument("record_ids", type=str)
@click.option("--output", "-o", required=True, help="Output file path")
@click.pass_context
@handle_error
def report_render(ctx, report_name, record_ids, output):
    """Render a report as PDF."""
    id_list = [int(x.strip()) for x in record_ids.split(",")]
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = report_mod.render(rpc, report_name, id_list, output)
    sess = get_session()
    sess.record("report render", {"report": report_name, "ids": id_list}, result)
    output_msg = f"Report saved: {result['output']} ({result['size']:,} bytes)"
    if _json_output:
        click.echo(json.dumps(result, indent=2, default=str))
    else:
        click.echo(output_msg)


@report.command("list")
@click.option("--model", "-m", default=None, help="Filter by model")
@click.pass_context
@handle_error
def report_list(ctx, model):
    """List available reports."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = report_mod.list_reports(rpc, model=model)
    output(result, f"Found {result['count']} report(s)")


# ── Export Command Group ───────────────────────────────────────

@cli.group()
def export():
    """Data export — CSV and JSON export."""
    pass


@export.command("csv")
@click.argument("model")
@click.option("--output", "-o", required=True, help="Output CSV file")
@click.option("--fields", "-f", default="", help="Comma-separated fields")
@click.option("--domain", "-d", default="[]", help="Search domain")
@click.option("--limit", "-l", type=int, default=0, help="Max records (0=all)")
@click.option("--order", default="", help="Sort order")
@click.pass_context
@handle_error
def export_csv(ctx, model, output, fields, domain, limit, order):
    """Export records to CSV."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = export_mod.export_csv(rpc, model, output, fields, domain, limit, order)
    sess = get_session()
    sess.record("export csv", {"model": model, "output": output}, result)
    if _json_output:
        click.echo(json.dumps(result, indent=2, default=str))
    else:
        click.echo(f"Exported {result['record_count']} records to {result['output']}")


@export.command("json")
@click.argument("model")
@click.option("--output", "-o", default=None, help="Output JSON file (prints to stdout if omitted)")
@click.option("--fields", "-f", default="", help="Comma-separated fields")
@click.option("--domain", "-d", default="[]", help="Search domain")
@click.option("--limit", "-l", type=int, default=0, help="Max records (0=all)")
@click.option("--order", default="", help="Sort order")
@click.pass_context
@handle_error
def export_json(ctx, model, output, fields, domain, limit, order):
    """Export records as JSON."""
    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))
    result = export_mod.export_json(rpc, model, output, fields, domain, limit, order)
    sess = get_session()
    sess.record("export json", {"model": model}, {"record_count": result.get("record_count")})
    if _json_output or not output:
        click.echo(json.dumps(result, indent=2, default=str))
    else:
        click.echo(f"Exported {result['record_count']} records to {result.get('output', 'stdout')}")


# ── Session Command Group ──────────────────────────────────────

@cli.group("session")
def session_group():
    """Session management — history, undo, redo."""
    pass


@session_group.command("status")
def session_status():
    """Show session status."""
    sess = get_session()
    output(sess.status())


@session_group.command("history")
@click.option("--limit", "-n", type=int, default=20, help="Max entries")
def session_history(limit):
    """Show command history."""
    sess = get_session()
    entries = sess.history(limit=limit)
    if not entries:
        output([], "No history.")
        return
    output(entries, f"History ({len(entries)} entries):")


@session_group.command("undo")
def session_undo():
    """Undo last command."""
    sess = get_session()
    entry = sess.undo()
    if entry:
        output(entry.to_dict(), f"Undone: {entry.command}")
    else:
        output({"error": "Nothing to undo"}, "Nothing to undo")


@session_group.command("redo")
def session_redo():
    """Redo last undone command."""
    sess = get_session()
    entry = sess.redo()
    if entry:
        output(entry.to_dict(), f"Redone: {entry.command}")
    else:
        output({"error": "Nothing to redo"}, "Nothing to redo")


# ── NLQ Ask Command ───────────────────────────────────────────

@cli.command("ask")
@click.argument("query", nargs=-1, required=True)
@click.option("--limit", "-l", type=int, default=50, help="Max records (default 50)")
@click.pass_context
@handle_error
def ask_cmd(ctx, query, limit):
    """Natural language query — ask in plain PT-BR, EN, or ES.

    Examples:
        ask "liste todas as empresas"
        ask "faturas de entrada pagas em janeiro/2024"
        ask "clientes ativos" --limit 10
        ask "vendas do pos de janeiro agrupado por categoria de produto"
    """
    query_str = " ".join(query)
    parsed = nlq_mod.parse(query_str)
    if limit != 50:
        parsed.limit = limit

    rpc = _get_rpc(ctx.obj["url"], ctx.obj["db"], ctx.obj["user"], ctx.obj["password"], ctx.obj.get("backend"))

    from cli_anything.odoo_12.utils.repl_skin import ReplSkin
    skin = ReplSkin("odoo_12", version="1.0.0")

    # Cross-model grouping (e.g. POS orders by product category)
    if parsed.cross_group_key:
        defn_key = (parsed.model, parsed.cross_group_key)
        defn = cg_mod.CROSS_GROUP_REGISTRY.get(defn_key)
        if defn:
            # Fetch parent IDs (all matching the domain)
            parent_ids = rpc.search(parsed.model, parsed.domain, limit=parsed.limit)

            sess = get_session()
            sess.record("ask", {"query": query_str, "model": parsed.model,
                                "cross_group": parsed.cross_group_key},
                        {"parent_count": len(parent_ids)})

            cg_result = cg_mod.execute_cross_group(rpc, parent_ids, defn)
            result = fmt_mod.format_cross_group_output(
                skin, parsed, cg_result, json_mode=_json_output,
            )
            if _json_output:
                click.echo(json.dumps(result, indent=2, default=str))
            return

    # Standard query (flat or simple group-by)
    search_result = record_mod.search(
        rpc, parsed.model, repr(parsed.domain),
        ",".join(parsed.fields), parsed.limit,
    )
    records = search_result.get("records", [])

    sess = get_session()
    sess.record("ask", {"query": query_str, "model": parsed.model},
                {"count": len(records)})

    if _json_output:
        result = fmt_mod.format_business_output(skin, parsed, records, json_mode=True)
        if parsed.export:
            fmt_mod._do_export(parsed, records)
            result["exported"] = parsed.export.path
        click.echo(json.dumps(result, indent=2, default=str))
    else:
        fmt_mod.format_business_output(skin, parsed, records)


# ── REPL ───────────────────────────────────────────────────────

@cli.command("repl", hidden=True)
@click.pass_context
def repl(ctx):
    """Enter interactive REPL mode."""
    global _repl_mode
    _repl_mode = True

    from cli_anything.odoo_12.utils.repl_skin import ReplSkin

    skin = ReplSkin("odoo_12", version="1.0.0")
    skin.print_banner()

    # Try to connect on REPL start
    try:
        info = conn_mod.connect(
            url=ctx.obj.get("url"), db=ctx.obj.get("db"),
            user=ctx.obj.get("user"), password=ctx.obj.get("password"),
            backend=ctx.obj.get("backend"),
        )
        skin.success(f"Connected to {info['url']} db={info['database']} uid={info['uid']}")
    except Exception as e:
        skin.warning(f"Not connected: {e}")
        skin.hint("Use 'connect --url ... --db ... --user ... --password ...' to connect")

    pt_session = skin.create_prompt_session()

    commands = {
        "ask <query>": "Natural language query (PT-BR, EN, ES)",
        "connect": "Connect to Odoo instance",
        "config show/delete": "Manage saved configuration",
        "record search <model>": "Search records with domain",
        "record read <model> <ids>": "Read records by ID",
        "record create <model>": "Create a new record",
        "record write <model> <ids>": "Update records",
        "record unlink <model> <ids>": "Delete records",
        "record count <model>": "Count matching records",
        "record fields <model>": "List model fields",
        "module list": "List modules",
        "module info <name>": "Module details",
        "module install <name>": "Install module",
        "module update <name>": "Update module",
        "db list": "List databases",
        "db create <name>": "Create database",
        "db backup <name> <path>": "Backup database",
        "db restore <name> <path>": "Restore database",
        "db drop <name>": "Drop database",
        "report render <name> <ids>": "Render report as PDF",
        "report list": "List available reports",
        "export csv <model>": "Export to CSV",
        "export json <model>": "Export as JSON",
        "session history": "Show command history",
        "session undo": "Undo last command",
        "session redo": "Redo last command",
        "help": "Show this help",
        "quit / exit": "Exit REPL",
    }

    while True:
        try:
            cfg = conn_mod.load_config()
            ctx_label = cfg.get("db", "")
            line = skin.get_input(pt_session, context=ctx_label)
        except (EOFError, KeyboardInterrupt):
            skin.print_goodbye()
            break

        if not line:
            continue
        if line in ("quit", "exit", "q"):
            skin.print_goodbye()
            break
        if line == "help":
            skin.help(commands)
            continue

        parts = line.split()
        try:
            cli.main(parts, standalone_mode=False)
        except SystemExit:
            pass
        except click.exceptions.UsageError as e:
            skin.error(str(e))
        except Exception as e:
            skin.error(str(e))


def main():
    cli()


if __name__ == "__main__":
    main()
