use clap::{Parser, Subcommand};

mod commands;
mod json_io;
mod route_parser;
#[cfg(feature = "repl")]
mod repl;
#[cfg(feature = "tui")]
mod tui;

#[derive(Parser)]
#[command(
    name = "rok",
    version,
    about = "rok developer CLI for Axum applications"
)]
struct Cli {
    /// Output result as JSON (machine-readable)
    #[arg(long, short = 'j', global = true)]
    json: bool,

    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Scaffold a new rok/Axum application
    New {
        /// Name of the application directory to create
        name: String,
        /// Project template: api, saas, htmx, microservice, minimal (default: interactive prompt)
        #[arg(long, short = 't', value_name = "TEMPLATE")]
        template: Option<String>,
    },

    // ── make:* generators ──────────────────────────────────────────────────────
    /// Generate a controller
    #[command(name = "make:controller")]
    MakeController {
        /// Controller name (e.g. User, BlogPost)
        name: String,
        /// Generate a full resource controller with CRUD methods
        #[arg(long)]
        resource: bool,
    },

    /// Generate a model (and optionally a migration, controller, resource, and factory)
    #[command(name = "make:model")]
    MakeModel {
        /// Model name (e.g. User, BlogPost)
        name: String,
        /// Primary key type: ulid, cuid2, uuid_v7, snowflake, nanoid (default: bigserial i64)
        #[arg(long, value_name = "TYPE")]
        id: Option<String>,
        /// Also generate a migration for this model
        #[arg(long)]
        migration: bool,
        /// Also generate a controller for this model
        #[arg(long)]
        controller: bool,
        /// Also generate an API resource transformer for this model
        #[arg(long)]
        resource: bool,
        /// Also generate a model factory for this model
        #[arg(long)]
        factory: bool,
        /// Generate everything: model + migration + controller + resource + create/update requests +
        /// policy + service + test (9 files)
        #[arg(long)]
        all: bool,
    },

    /// Generate a migration
    #[command(name = "make:migration")]
    MakeMigration {
        /// Migration name (e.g. create_users_table)
        name: String,
    },

    /// Generate a seeder
    #[command(name = "make:seeder")]
    MakeSeeder {
        /// Seeder name (e.g. User, BlogPost)
        name: String,
    },

    /// Generate a request validation struct
    #[command(name = "make:request")]
    MakeRequest {
        /// Request name (e.g. CreatePost, Login)
        name: String,
        /// Generate a create+update request pair for the given resource
        #[arg(long)]
        resource: bool,
    },

    /// Generate a validator
    #[command(name = "make:validator")]
    MakeValidator {
        /// Validator name (e.g. Login, CreatePost)
        name: String,
    },

    /// Generate a complete authentication system
    #[command(name = "make:auth")]
    MakeAuth {
        /// Auth guard: jwt or session (default: jwt)
        #[arg(long, default_value = "jwt")]
        guard: String,
        /// Comma-separated extra features: email-verify, password-reset, totp (default: email-verify,password-reset)
        #[arg(long, default_value = "email-verify,password-reset")]
        features: String,
        /// User model name (default: User)
        #[arg(long, default_value = "User")]
        model: String,
        /// Overwrite existing files
        #[arg(long)]
        force: bool,
    },

    /// Generate full CRUD scaffold: model, migration, controller, validator, resource, policy, factory, test, routes
    #[command(name = "make:crud")]
    MakeCrud {
        /// Resource name (e.g. Post, BlogArticle)
        name: String,
        /// Comma-separated fields: title:string,body:text,published:bool
        #[arg(long, value_name = "FIELDS")]
        fields: Option<String>,
        /// Primary key type: ulid, cuid2, uuid_v7, snowflake, nanoid (default: bigserial)
        #[arg(long, value_name = "TYPE")]
        id: Option<String>,
        /// Add auth guard (require authenticated user) to all routes
        #[arg(long)]
        auth: bool,
        /// Add deleted_at soft-delete column
        #[arg(long)]
        soft_delete: bool,
        /// Add created_at / updated_at timestamp columns
        #[arg(long, default_value = "true")]
        timestamps: bool,
        /// Add cursor-based pagination to index endpoint
        #[arg(long)]
        paginate: bool,
        /// Overwrite existing files
        #[arg(long)]
        force: bool,
    },

    /// Generate a model + migration from an inline JSON payload
    #[command(name = "make:from-json")]
    MakeFromJson {
        /// Resource name (e.g. Order)
        name: String,
        /// JSON string (inline) or path to a JSON file (use with --file)
        json_input: String,
        /// Treat `json` argument as a file path
        #[arg(long)]
        file: bool,
        /// Preview generated code without writing files
        #[arg(long)]
        dry_run: bool,
        /// Overwrite existing files
        #[arg(long)]
        force: bool,
    },

    /// Generate a model + migration from a JSON Schema file
    #[command(name = "make:from-schema")]
    MakeFromSchema {
        /// Resource name (e.g. Order)
        name: String,
        /// Path to the JSON Schema file
        #[arg(long, value_name = "FILE")]
        file: String,
        /// Preview generated code without writing files
        #[arg(long)]
        dry_run: bool,
        /// Overwrite existing files
        #[arg(long)]
        force: bool,
    },

    /// Generate an observer
    #[command(name = "make:observer")]
    MakeObserver {
        /// Observer name (e.g. User, Post)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate an API resource transformer
    #[command(name = "make:resource")]
    MakeResource {
        /// Resource name (e.g. User, Post)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate a query scope
    #[command(name = "make:scope")]
    MakeScope {
        /// Scope name (e.g. Active, Published)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate an authorization policy
    #[command(name = "make:policy")]
    MakePolicy {
        /// Policy name (e.g. User, Post)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate a background job
    #[command(name = "make:job")]
    MakeJob {
        /// Job name (e.g. SendWelcomeEmail)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate an event
    #[command(name = "make:event")]
    MakeEvent {
        /// Event name (e.g. UserRegistered)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate an event listener
    #[command(name = "make:listener")]
    MakeListener {
        /// Listener name (e.g. SendWelcomeEmail)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate a notification
    #[command(name = "make:notification")]
    MakeNotification {
        /// Notification name (e.g. WelcomeMail)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate a test file
    #[command(name = "make:test")]
    MakeTest {
        /// Test name (e.g. UserController)
        name: String,
        /// Overwrite existing file
        #[arg(long)]
        force: bool,
    },

    /// Generate a locale translation file from locales/en.json as template
    #[command(name = "make:locale")]
    MakeLocale {
        /// Language code to generate (e.g. fr, de, es, ja)
        lang: String,
        /// Overwrite if the locale file already exists
        #[arg(long)]
        force: bool,
    },

    /// Generate a TypeScript API client from resource structs
    #[command(name = "make:ts-client")]
    MakeTsClient {
        /// Output file path (default: client.ts)
        #[arg(long, value_name = "FILE")]
        output: Option<String>,
    },

    // ── db:* commands ──────────────────────────────────────────────────────────
    /// Apply all pending migrations
    #[command(name = "db:migrate")]
    DbMigrate {
        /// Print pending migrations without applying them
        #[arg(long)]
        dry_run: bool,
    },

    /// Rollback the last N migration batches
    #[command(name = "db:rollback")]
    DbRollback {
        /// Number of migration steps to roll back (default 1)
        #[arg(long, value_name = "N")]
        step: Option<u32>,
    },

    /// Show migration status
    #[command(name = "db:status")]
    DbStatus,

    /// Apply pending migrations with progress output
    #[command(name = "db:update")]
    DbUpdate,

    /// Run database seeders
    #[command(name = "db:seed")]
    DbSeed {
        /// Run only this specific seeder class
        #[arg(long, value_name = "CLASS")]
        class: Option<String>,
    },

    /// Drop all tables and re-run all migrations
    #[command(name = "db:fresh")]
    DbFresh,

    /// Introspect a live database table and generate a model struct
    #[command(name = "db:pull")]
    DbPull {
        /// Table name to introspect
        table: String,
        /// Write generated model to this file instead of printing
        #[arg(long, value_name = "FILE")]
        output: Option<String>,
    },

    /// Compare a model file against the live database schema and report drift
    #[command(name = "db:diff")]
    DbDiff {
        /// Path to the model source file (e.g. src/app/models/user.rs)
        model_file: String,
        /// Override table name (default: inferred from #[rok_orm(table = "...")])
        #[arg(long, value_name = "TABLE")]
        table: Option<String>,
    },

    /// Run EXPLAIN ANALYZE on a raw SQL query and print the query plan
    #[command(name = "db:explain")]
    DbExplain {
        /// SQL query to explain (e.g. "SELECT * FROM users WHERE active = true")
        query: String,
        /// Database URL (reads DATABASE_URL env var if not provided)
        #[arg(long, value_name = "URL")]
        db: Option<String>,
    },

    /// Roll back all migrations then re-run them all (reset the DB)
    #[command(name = "db:reset")]
    DbReset,

    /// Display model fields, casts, scopes, and relationships from source
    #[command(name = "model:show")]
    ModelShow {
        /// Model name or path (e.g. User or src/app/models/user.rs)
        model: String,
    },

    /// Cache config and routes for production
    #[command(name = "optimize")]
    Optimize,

    /// Clear the production cache generated by `rok optimize`
    #[command(name = "optimize:clear")]
    OptimizeClear,

    // ── queue:* commands ───────────────────────────────────────────────────────
    /// Start the queue worker (hint: integrate rok_queue::Worker in your app)
    #[command(name = "queue:work")]
    QueueWork {
        /// Queue name to consume (default: "default")
        #[arg(long, value_name = "QUEUE")]
        queue: Option<String>,
        /// Number of concurrent job runners (default: 4)
        #[arg(long, value_name = "N")]
        concurrency: Option<usize>,
    },

    /// Show pending/running/done/failed job counts per queue
    #[command(name = "queue:status")]
    QueueStatus,

    /// Reset a failed job back to pending
    #[command(name = "queue:retry")]
    QueueRetry {
        /// Job ID to retry
        #[arg(long, value_name = "ID")]
        job_id: i64,
    },

    /// Delete failed jobs from a queue (default: all queues)
    #[command(name = "queue:flush")]
    QueueFlush {
        /// Queue name (omit to flush all queues)
        #[arg(long, value_name = "QUEUE")]
        queue: Option<String>,
    },

    // ── key / routes / dev / env ───────────────────────────────────────────────
    /// Generate a cryptographically random JWT secret
    #[command(name = "key:generate")]
    KeyGenerate,

    /// Read .env.example, generate crypto-random values for all secret-like keys,
    /// and write .env
    #[command(name = "secrets:generate")]
    SecretsGenerate,

    /// Rotate APP_KEY / JWT_SECRET — generate a new key and optionally update .env
    #[command(name = "key:rotate")]
    KeyRotate {
        /// Write the new key to .env in-place (replaces the matching KEY= line)
        #[arg(long)]
        write: bool,
        /// Key name to rotate (default: JWT_SECRET)
        #[arg(long, value_name = "NAME", default_value = "JWT_SECRET")]
        key: String,
    },

    /// Print current environment / config values (secrets are masked)
    #[command(name = "config:show")]
    ConfigShow {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// List all registered routes
    #[command(name = "routes:list")]
    RoutesList,

    /// List all registered scheduled jobs
    #[command(name = "schedule:list")]
    ScheduleList,

    /// Start the dev server with file-watch auto-restart (requires cargo-watch)
    Dev,

    /// Start the application server (auto-reloads with cargo-watch if available)
    Serve {
        /// Port to listen on (sets LISTEN_ADDR env var)
        #[arg(long, value_name = "PORT")]
        port: Option<u16>,
        /// Build in release mode
        #[arg(long)]
        release: bool,
    },

    /// Check that all required env vars (from #[env(...)]) are set
    #[command(name = "env:check")]
    EnvCheck,

    /// Write a .env file from config struct defaults and .env.example template
    #[command(name = "env:generate")]
    EnvGenerate {
        /// Overwrite an existing .env without confirmation
        #[arg(long)]
        force: bool,
    },

    /// Create a .rok/config.toml with generator defaults
    #[command(name = "rok:init")]
    RokInit,

    /// List all available commands grouped by category
    #[command(name = "list")]
    List,

    /// Check project structure, required files, and dependencies
    #[command(name = "check")]
    Check,

    /// Generate shell completions
    #[command(name = "completion")]
    Completion {
        /// Shell type: bash, zsh, fish, powershell, elvish
        shell: Option<String>,
    },

    /// Launch the terminal UI dashboard (DB browser, migrations, features, queue)
    #[cfg(feature = "tui")]
    Tui {
        /// Database URL override (defaults to DATABASE_URL env var)
        #[arg(long, value_name = "URL")]
        db: Option<String>,
    },

    /// Start an interactive REPL with direct database access (requires DATABASE_URL)
    #[cfg(feature = "repl")]
    Tinker,

    /// Put the application into maintenance mode
    Down {
        /// Bypass secret token (auto-generated if omitted)
        #[arg(long)]
        secret: Option<String>,
        /// Custom maintenance message
        #[arg(long)]
        message: Option<String>,
        /// Allow retry after N seconds (returns Retry-After header)
        #[arg(long)]
        retry: Option<u64>,
    },

    /// Bring the application out of maintenance mode
    Up,

    /// Check maintenance mode status
    #[command(name = "maintenance:status")]
    MaintenanceStatus,

    // ── make:scaffold / make:feature ──────────────────────────────────────────
    /// One-command feature scaffold (auth, crud, crud-api, upload, webhook, ...)
    #[command(name = "make:scaffold")]
    MakeScaffold {
        /// Template name (run without args to list all)
        template: Option<String>,
        /// Resource name (used as prefix for generated files)
        #[arg(long, short = 'n', value_name = "NAME")]
        name: Option<String>,
        /// Model name for data-centric scaffolds
        #[arg(long, short = 'm', value_name = "MODEL")]
        model: Option<String>,
        /// Preview what would be generated without writing files
        #[arg(long)]
        dry_run: bool,
        /// Read scaffold config from JSON file
        #[arg(long, short = 'J', alias = "jf", value_name = "FILE")]
        json_file: Option<String>,
        /// List all available scaffold templates
        #[arg(long, short = 'l')]
        list: bool,
    },

    /// Generate a complete feature from a JSON spec (models + controllers + routes)
    #[command(name = "make:feature")]
    MakeFeature {
        /// Inline JSON spec string
        #[arg(long, value_name = "JSON")]
        spec: Option<String>,
        /// Path to JSON spec file
        #[arg(long, short = 'J', alias = "jf", value_name = "FILE")]
        json_file: Option<String>,
        /// Preview without writing files
        #[arg(long)]
        dry_run: bool,
    },

    // ── plan:* commands ───────────────────────────────────────────────────────
    /// Show the next incomplete phase in router.md
    #[command(name = "plan:next")]
    PlanNext,

    /// List all phases with their status
    #[command(name = "plan:list")]
    PlanList,

    /// Output the dependency graph in DOT format
    #[command(name = "plan:graph")]
    PlanGraph,

    /// Show status of a specific phase (or all phases)
    #[command(name = "plan:status")]
    PlanStatus {
        /// Phase number to inspect
        #[arg(long, value_name = "N")]
        phase: Option<u32>,
    },

    // ── agent:* commands ──────────────────────────────────────────────────────
    /// Print the rok coding rules injected into AI agents
    #[command(name = "agent:rules")]
    AgentRules,

    /// Show full context (feature.md + PRD + prompt + progress) for a phase
    #[command(name = "agent:context")]
    AgentContext {
        /// Phase number
        #[arg(long, value_name = "N")]
        phase: Option<u32>,
    },

    /// Append feedback to progress.txt
    #[command(name = "agent:feedback")]
    AgentFeedback {
        /// Progress note to append
        #[arg(long, value_name = "TEXT")]
        progress: String,
        /// PRD item index to mark updated (0-based)
        #[arg(long, value_name = "N")]
        prd_item: Option<usize>,
    },

    /// Launch Claude Code with rok rules pre-injected
    #[command(name = "agent:claude")]
    AgentClaude {
        /// Phase number to provide as context
        #[arg(long, value_name = "N")]
        phase: Option<u32>,
        /// Additional prompt text
        #[arg(long, value_name = "TEXT")]
        prompt: Option<String>,
    },

    /// Launch OpenCode with rok rules pre-injected
    #[command(name = "agent:opencode")]
    AgentOpencode {
        /// Phase number to provide as context
        #[arg(long, value_name = "N")]
        phase: Option<u32>,
    },

    // ── publish ───────────────────────────────────────────────────────────────
    /// Publish all crates to crates.io in dependency order (runs gates, creates tags)
    #[command(name = "publish")]
    Publish {
        /// Verify gates + show publish order without uploading
        #[arg(long)]
        dry_run: bool,
        /// Publish only this specific crate
        #[arg(long, value_name = "CRATE")]
        crate_name: Option<String>,
    },

    /// Bump version, commit, tag, push, and publish (patch|minor|major)
    #[command(name = "release")]
    Release {
        /// Version bump type: patch, minor, or major
        bump: String,
        /// Release only this specific crate
        #[arg(long, value_name = "CRATE")]
        crate_name: Option<String>,
        /// Skip the publish step after release
        #[arg(long)]
        skip_publish: bool,
    },

    /// Preview unreleased commits grouped by type
    #[command(name = "changelog")]
    Changelog,

    // ── openapi ──────────────────────────────────────────────────────────────
    /// Generate OpenAPI spec from code
    #[command(name = "openapi:generate")]
    OpenapiGenerate {
        /// Output file path (default: openapi.json)
        #[arg(long, short = 'o', value_name = "FILE")]
        output: Option<String>,
        /// Output format: json or yaml
        #[arg(long, default_value = "json")]
        format: String,
    },

    /// Serve OpenAPI spec preview with Swagger UI
    #[command(name = "openapi:serve")]
    OpenapiServe {
        /// Port to listen on (default: 3001)
        #[arg(long, short = 'p', value_name = "PORT")]
        port: Option<u16>,
    },

    /// Validate an OpenAPI spec file
    #[command(name = "openapi:validate")]
    OpenapiValidate {
        /// Path to OpenAPI spec file (default: openapi.json)
        file: Option<String>,
    },

    // ── doctor ───────────────────────────────────────────────────────────────
    /// Run full project diagnostics and flag missing dependencies or config
    Doctor {
        /// Auto-resolve safe issues (copy .env.example → .env, etc.)
        #[arg(long)]
        fix: bool,
    },

    // ── metrics ──────────────────────────────────────────────────────────────
    /// Show a live metrics dashboard from your app's /metrics endpoint
    Metrics {
        /// URL of the Prometheus metrics endpoint (default: http://localhost:3000/metrics)
        url: Option<String>,
        /// Refresh interval in seconds (default: 2)
        #[arg(long, value_name = "SECS")]
        interval: Option<u64>,
    },

    // ── api:test ─────────────────────────────────────────────────────────────
    /// Make an HTTP request and pretty-print the response
    #[command(name = "api:test")]
    ApiTest {
        /// Target URL
        url: Option<String>,
        /// HTTP method (default: GET)
        #[arg(long, short = 'X', value_name = "METHOD")]
        method: Option<String>,
        /// Request body (JSON string)
        #[arg(long, short = 'd', value_name = "JSON")]
        data: Option<String>,
        /// Add request header (K:V, repeatable)
        #[arg(long, short = 'H', value_name = "K:V")]
        header: Vec<String>,
    },

    // ── deploy:check ─────────────────────────────────────────────────────────
    /// Run pre-deploy validation gates (git clean, release build, env vars, etc.)
    #[command(name = "deploy:check")]
    DeployCheck,

    // ── upgrade ──────────────────────────────────────────────────────────────
    /// Show rok-* dependency versions and optionally patch Cargo.lock
    Upgrade {
        /// Run `cargo update` to bump all rok-* crates to latest patch
        #[arg(long)]
        patch: bool,
    },

    // ── bench ────────────────────────────────────────────────────────────────
    /// Benchmark a URL with oha/wrk/ab (auto-detects installed tool)
    Bench {
        /// Target URL to benchmark
        url: String,
        /// Total requests (default: 1000)
        #[arg(long, short = 'n', value_name = "N")]
        requests: Option<u64>,
        /// Duration in seconds (overrides --requests if set)
        #[arg(long, short = 'z', value_name = "SECS")]
        duration: Option<u64>,
        /// Concurrent connections (default: 10)
        #[arg(long, short = 'c', value_name = "N")]
        concurrency: Option<u64>,
    },

    // ── profile ──────────────────────────────────────────────────────────────
    /// Profile a binary with flamegraph/perf/Instruments (auto-detects tool)
    Profile {
        /// Binary name to profile (from [[bin]] in Cargo.toml)
        #[arg(long, value_name = "BIN")]
        target: Option<String>,
        /// Output file stem (default: profile_output)
        #[arg(long, short = 'o', value_name = "FILE")]
        output: Option<String>,
    },

    // ── plugin:* ─────────────────────────────────────────────────────────────
    /// Install a rok plugin crate
    #[command(name = "plugin:install")]
    PluginInstall {
        /// Plugin name or crate (e.g. `stripe`, `rok-plugin-stripe`)
        name: String,
        /// Crate name to install (overrides the default rok-plugin-<name>)
        #[arg(long, value_name = "CRATE")]
        source: Option<String>,
    },

    /// List installed rok plugins
    #[command(name = "plugin:list")]
    PluginList,

    /// Remove an installed rok plugin
    #[command(name = "plugin:remove")]
    PluginRemove {
        /// Plugin name to remove
        name: String,
    },

    // ── self:* commands ──────────────────────────────────────────────────────
    /// Check for updates to rok-cli
    #[command(name = "self-update")]
    SelfUpdate {
        /// Install a specific version instead of latest
        #[arg(long, value_name = "VER")]
        version: Option<String>,
        /// Re-install even if already up-to-date
        #[arg(long)]
        force: bool,
        /// Show what would be updated without making changes
        #[arg(long)]
        dry_run: bool,
    },

    /// Migrate local configuration for a new rok-cli version
    #[command(name = "self-migrate")]
    SelfMigrate {
        /// Migrate from this version (default: current config version)
        #[arg(long, value_name = "VER")]
        from: Option<String>,
        /// Migrate to this version (default: current CLI version)
        #[arg(long, value_name = "VER")]
        to: Option<String>,
        /// Show what would be migrated without making changes
        #[arg(long)]
        dry_run: bool,
    },

    /// Show installed version, latest available, and migration status
    #[command(name = "self-status")]
    SelfStatus,
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();
    let json_mode = cli.json;

    let result: anyhow::Result<()> = match cli.command {
        Command::New { name, template } => commands::new::run(&name, template.as_deref()),

        // ── make:* ────────────────────────────────────────────────────────────
        Command::MakeController { name, resource } => commands::make::controller(&name, resource),
        Command::MakeModel {
            name,
            id,
            migration,
            controller,
            resource,
            factory,
            all,
        } => {
            if all {
                commands::make::model_all(&name, id.as_deref())
            } else {
                commands::make::model_full(
                    &name,
                    id.as_deref(),
                    migration,
                    controller,
                    resource,
                    factory,
                )
            }
        }
        Command::MakeMigration { name } => commands::make::migration(&name),
        Command::MakeSeeder { name } => commands::make::seeder(&name),
        Command::MakeRequest { name, resource } => {
            if resource {
                commands::make::make_request_pair(&name)
            } else {
                commands::make::request(&name)
            }
        }
        Command::MakeValidator { name } => commands::make::validator(&name),
        Command::MakeObserver { name, force } => commands::make::observer(&name, force),
        Command::MakeResource { name, force } => commands::make::resource(&name, force),
        Command::MakeScope { name, force } => commands::make::scope(&name, force),
        Command::MakePolicy { name, force } => commands::make::policy(&name, force),
        Command::MakeJob { name, force } => commands::make::job(&name, force),
        Command::MakeEvent { name, force } => commands::make::event(&name, force),
        Command::MakeListener { name, force } => commands::make::listener(&name, force),
        Command::MakeNotification { name, force } => commands::make::notification(&name, force),
        Command::MakeTest { name, force } => commands::make::test(&name, force),
        Command::MakeLocale { lang, force } => commands::make::locale(&lang, force),

        Command::MakeCrud {
            name,
            fields,
            id,
            auth,
            soft_delete,
            timestamps,
            paginate,
            force,
        } => commands::crud::crud(
            &name,
            fields.as_deref(),
            id.as_deref(),
            auth,
            soft_delete,
            paginate,
            timestamps,
            force,
        ),

        Command::MakeAuth {
            guard,
            features,
            model,
            force,
        } => commands::make::auth(&guard, &features, &model, force),

        Command::MakeFromJson {
            name,
            json_input,
            file,
            dry_run,
            force,
        } => commands::from_json::from_json(&name, &json_input, file, dry_run, force),

        Command::MakeFromSchema {
            name,
            file,
            dry_run,
            force,
        } => commands::from_json::from_schema(&name, &file, dry_run, force),

        Command::MakeTsClient { output } => commands::ts_client::make_ts_client(output.as_deref()),

        // ── db:* ──────────────────────────────────────────────────────────────
        Command::DbMigrate { dry_run } => commands::db::migrate(dry_run).await,
        Command::DbRollback { step } => commands::db::rollback(step.unwrap_or(1)).await,
        Command::DbStatus => commands::db::status().await,
        Command::DbUpdate => commands::db::update().await,
        Command::DbSeed { class } => commands::db::seed(class.as_deref()).await,
        Command::DbFresh => commands::db::fresh().await,

        Command::DbPull { table, output } => {
            commands::inspect::pull(&table, output.as_deref()).await
        }
        Command::DbDiff { model_file, table } => {
            commands::inspect::diff(&model_file, table.as_deref()).await
        }
        Command::DbExplain { query, db } => {
            commands::db::explain(&query, db.as_deref()).await
        }
        Command::DbReset => commands::db::reset().await,
        Command::ModelShow { model } => commands::inspect::model_show(&model),
        Command::Optimize => commands::dev::optimize(),
        Command::OptimizeClear => commands::dev::optimize_clear(),

        // ── queue:* ───────────────────────────────────────────────────────────
        Command::QueueWork { queue, concurrency } => {
            commands::queue::work(queue.as_deref(), concurrency)
        }
        Command::QueueStatus => commands::queue::status().await,
        Command::QueueRetry { job_id } => commands::queue::retry(job_id).await,
        Command::QueueFlush { queue } => commands::queue::flush(queue.as_deref()).await,

        // ── utilities ─────────────────────────────────────────────────────────
        Command::KeyGenerate => {
            commands::key::run();
            Ok(())
        }
        Command::SecretsGenerate => commands::secrets::run(),
        Command::KeyRotate { write, key } => commands::key::rotate(&key, write),
        Command::ConfigShow { json } => commands::dev::config_show(json),
        Command::RoutesList => commands::routes::run(),
        Command::ScheduleList => commands::schedule_list::run(),
        Command::Dev => commands::dev::dev(),
        Command::Serve { port, release } => commands::dev::serve(port, release),
        Command::EnvCheck => commands::dev::env_check(),
        Command::EnvGenerate { force } => commands::dev::env_generate(force),
        Command::RokInit => commands::dev::init_rok_config(),
        Command::List => {
            commands::list::run();
            Ok(())
        }
        Command::Check => {
            commands::check::run();
            Ok(())
        }
        Command::Completion { shell } => {
            commands::completion::run(shell);
            Ok(())
        }

        #[cfg(feature = "tui")]
        Command::Tui { db } => commands::tui::run(db.as_deref()),
        #[cfg(feature = "repl")]
        Command::Tinker => commands::repl::run().await,

        // ── maintenance mode ──────────────────────────────────────────────────
        Command::Down {
            secret,
            message,
            retry,
        } => (|| -> anyhow::Result<()> {
            commands::down::activate(secret, message, retry)
        })(),
        Command::Up => (|| -> anyhow::Result<()> {
            commands::down::deactivate()
        })(),
        Command::MaintenanceStatus => (|| -> anyhow::Result<()> {
            commands::down::status()?;
            Ok(())
        })(),

        // ── make:scaffold / make:feature ──────────────────────────────────────
        Command::MakeScaffold {
            template,
            name,
            model,
            dry_run,
            json_file: _,
            list,
        } => {
            use commands::scaffolds::{ScaffoldArgs, ScaffoldRegistry};
            (|| -> anyhow::Result<()> {
                let reg = ScaffoldRegistry::new();
                if list || template.is_none() {
                    for (n, desc) in reg.list() {
                        println!("{:<20} {desc}", n);
                    }
                    return Ok(());
                }
                let tmpl = template.as_deref().unwrap();
                let scaffold = reg.get(tmpl).ok_or_else(|| {
                    anyhow::anyhow!("Unknown scaffold '{tmpl}' — run: rok make:scaffold --list")
                })?;
                let args = ScaffoldArgs {
                    name,
                    model,
                    fields: Vec::new(),
                    flags: Default::default(),
                    dry_run,
                    json: json_mode,
                };
                let result = scaffold.generate(&args)?;
                if json_mode {
                    result.print_json();
                } else {
                    result.print_human();
                }
                Ok(())
            })()
        }

        Command::MakeFeature {
            spec,
            json_file,
            dry_run,
        } => commands::feature::run(spec.as_deref(), json_file.as_deref(), dry_run, json_mode),

        // ── plan:* ────────────────────────────────────────────────────────────
        Command::PlanNext => commands::plan::next(json_mode),
        Command::PlanList => commands::plan::list(json_mode),
        Command::PlanGraph => commands::plan::graph(),
        Command::PlanStatus { phase } => commands::plan::status(phase, json_mode),

        // ── agent:* ───────────────────────────────────────────────────────────
        Command::AgentRules => commands::agent::rules(json_mode),
        Command::AgentContext { phase } => commands::agent::context(phase, json_mode),
        Command::AgentFeedback { progress, prd_item } => {
            commands::agent::feedback(&progress, prd_item, json_mode)
        }
        Command::AgentClaude { phase, prompt } => {
            commands::agent::launch_claude(phase, prompt.as_deref())
        }
        Command::AgentOpencode { phase } => commands::agent::launch_opencode(phase, None),

        // ── publish ──────────────────────────────────────────────────────────────
        Command::Publish {
            dry_run,
            crate_name,
        } => {
            commands::publish::run(dry_run, crate_name.as_deref());
            Ok(())
        }

        // ── release / changelog ───────────────────────────────────────────────
        Command::Release {
            bump,
            crate_name,
            skip_publish,
        } => {
            commands::release::run(&bump, crate_name.as_deref(), skip_publish);
            Ok(())
        }
        Command::Changelog => {
            commands::changelog::run();
            Ok(())
        }

        // ── openapi ─────────────────────────────────────────────────────────
        Command::OpenapiGenerate { output, format } => {
            commands::openapi::generate(output.as_deref(), &format)
        }
        Command::OpenapiServe { port } => commands::openapi::serve(port),
        Command::OpenapiValidate { file } => commands::openapi::validate(file.as_deref()),

        // ── doctor / metrics / api:test / deploy / upgrade / bench / profile / plugin ─
        Command::Doctor { fix } => commands::doctor::run(fix),
        Command::Metrics { url, interval } => commands::metrics::run(url.as_deref(), interval),
        Command::ApiTest { url, method, data, header } => {
            commands::api_test::run(method.as_deref(), url.as_deref(), data.as_deref(), header, json_mode)
        }
        Command::DeployCheck => commands::deploy::check(),
        Command::Upgrade { patch } => commands::upgrade::run(patch),
        Command::Bench { url, requests, duration, concurrency } => {
            commands::bench::run(&url, requests, duration, concurrency)
        }
        Command::Profile { target, output } => {
            commands::profile::run(target.as_deref(), output.as_deref())
        }
        Command::PluginInstall { name, source } => {
            commands::plugin::install(&name, source.as_deref())
        }
        Command::PluginList => commands::plugin::list(),
        Command::PluginRemove { name } => commands::plugin::remove(&name),

        // ── self:* ─────────────────────────────────────────────────────────
        Command::SelfUpdate {
            version,
            force,
            dry_run,
        } => commands::self_update::run_update(version.as_deref(), force, dry_run),
        Command::SelfMigrate { from, to, dry_run } => {
            commands::self_update::run_migrate(from.as_deref(), to.as_deref(), dry_run)
        }
        Command::SelfStatus => commands::self_update::run_check(),
    };

    if let Err(e) = result {
        eprintln!("{} {e:#}", console::style("error:").red().bold());
        std::process::exit(1);
    }
}
