#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

use Config ();
use Capture::Tiny qw(capture);
use Cwd qw(cwd);
use FindBin qw($Bin);
use lib "$Bin/../lib";
use File::Spec;
use Getopt::Long qw(GetOptionsFromArray);
use Getopt::Long ();

binmode STDOUT, ':encoding(UTF-8)';
binmode STDERR, ':encoding(UTF-8)';

use Developer::Dashboard::Platform qw(
  native_shell_name
  normalize_shell_name
  shell_quote_for
);
use Developer::Dashboard::CLI::SeededPages ();
use Developer::Dashboard::InternalCLI ();
use Developer::Dashboard::SeedSync ();

my $dashboard_lib = File::Spec->catdir( $Bin, '..', 'lib' );
if ( -d $dashboard_lib ) {
    my $path_sep = $Config::Config{path_sep} || ':';
    my @existing = grep { defined && $_ ne '' } split /\Q$path_sep\E/, ( $ENV{PERL5LIB} || '' );
    if ( !grep { $_ eq $dashboard_lib } @existing ) {
        $ENV{PERL5LIB} = join $path_sep, $dashboard_lib, @existing;
    }
}

my $cmd = shift @ARGV || die "Missing built-in dashboard command\n";

require Developer::Dashboard::Auth;
require Developer::Dashboard::ActionRunner;
require Developer::Dashboard::Codec;
Developer::Dashboard::Codec->import(qw(encode_payload decode_payload));
require Developer::Dashboard::CollectorRunner;
require Developer::Dashboard::Collector;
require Developer::Dashboard::Config;
require Developer::Dashboard;
require Developer::Dashboard::DockerCompose;
require Developer::Dashboard::Doctor;
require Developer::Dashboard::FileRegistry;
require Developer::Dashboard::IndicatorStore;
require Developer::Dashboard::JSON;
Developer::Dashboard::JSON->import(qw(json_encode));
require Developer::Dashboard::PageDocument;
require Developer::Dashboard::PageRuntime;
require Developer::Dashboard::PageResolver;
require Developer::Dashboard::PageStore;
require Developer::Dashboard::PathRegistry;
require Developer::Dashboard::Prompt;
require Developer::Dashboard::RuntimeManager;
require Developer::Dashboard::SessionStore;
require Developer::Dashboard::Web::App;
require Developer::Dashboard::Web::Server;

my $paths = Developer::Dashboard::PathRegistry->new(
    workspace_roots => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
    project_roots   => [ grep { defined && -d } map { "$ENV{HOME}/$_" } qw(projects src work) ],
);
_apply_runtime_perl5lib( paths => $paths );
my $files      = Developer::Dashboard::FileRegistry->new( paths => $paths );
my $indicators = Developer::Dashboard::IndicatorStore->new( paths => $paths );
my $collectors = Developer::Dashboard::Collector->new( paths => $paths );
my $runner     = Developer::Dashboard::CollectorRunner->new(
    collectors => $collectors,
    files      => $files,
    indicators => $indicators,
    paths      => $paths,
);
my $config     = Developer::Dashboard::Config->new( files => $files, paths => $paths );
my $pages      = Developer::Dashboard::PageStore->new( paths => $paths );
my $page_runtime = Developer::Dashboard::PageRuntime->new(
    files   => $files,
    paths   => $paths,
    aliases => $config->path_aliases,
);
my $auth = Developer::Dashboard::Auth->new(
    files => $files,
    paths => $paths,
);
my $actions = Developer::Dashboard::ActionRunner->new(
    files => $files,
    paths => $paths,
);
my $resolver = Developer::Dashboard::PageResolver->new(
    actions => $actions,
    config  => $config,
    pages   => $pages,
    paths   => $paths,
);
my $docker = Developer::Dashboard::DockerCompose->new(
    config  => $config,
    paths   => $paths,
);
my $doctor = Developer::Dashboard::Doctor->new(
    paths => $paths,
);
my $prompt = Developer::Dashboard::Prompt->new(
    paths      => $paths,
    indicators => $indicators,
);
my $sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
my $runtime = Developer::Dashboard::RuntimeManager->new(
    app_builder => sub {
        my (%args) = @_;
        my $web_settings = $config->web_settings;
        my $web_auth = Developer::Dashboard::Auth->new( files => $files, paths => $paths );
        my $web_pages = Developer::Dashboard::PageStore->new( paths => $paths );
        my $web_sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
        my $app = Developer::Dashboard::Web::App->new(
            actions  => $actions,
            auth     => $web_auth,
            config   => $config,
            pages    => $web_pages,
            prompt   => $prompt,
            runtime  => $page_runtime,
            resolver => $resolver,
            sessions => $web_sessions,
        );
        return Developer::Dashboard::Web::Server->new(
            app     => $app,
            host    => $args{host},
            port    => $args{port},
            workers => $args{workers},
            ssl     => $args{ssl},
            ssl_subject_alt_names => $web_settings->{ssl_subject_alt_names},
        );
    },
    config => $config,
    files  => $files,
    paths  => $paths,
    runner => $runner,
);

my $CONFIG_PATH_ALIASES_LOADED  = 0;
my $SAVED_PAGES_MIGRATED        = 0;
my $COLLECTOR_INDICATORS_SYNCED = 0;

# _prime_ticket_ref_env()
# Seeds TICKET_REF from the active tmux session when the shell environment does
# not already provide it.
# Input: none.
# Output: true after best-effort environment priming.
sub _prime_ticket_ref_env {
    return 1 if defined $ENV{TICKET_REF} && $ENV{TICKET_REF} ne '';
    my $ticket = _tmux_ticket_ref();
    $ENV{TICKET_REF} = $ticket if defined $ticket && $ticket ne '';
    return 1;
}

# _tmux_ticket_ref()
# Reads TICKET_REF from tmux session environment when available.
# Input: none.
# Output: ticket string or undef when tmux does not expose one.
sub _tmux_ticket_ref {
    my ( $stdout, undef, $exit_code ) = capture {
        system 'tmux', 'show-environment', 'TICKET_REF';
        return $? >> 8;
    };
    return if $exit_code != 0;
    return if !defined $stdout || $stdout eq '';
    for my $line ( split /\n/, $stdout ) {
        next if !defined $line || $line eq '' || $line =~ /^-/;
        return $1 if $line =~ /^TICKET_REF=(.+)$/;
    }
    return;
}

sub _load_configured_path_aliases {
    return 1 if $CONFIG_PATH_ALIASES_LOADED;
    $paths->register_named_paths( $config->path_aliases );
    $CONFIG_PATH_ALIASES_LOADED = 1;
    return 1;
}

sub _migrate_saved_pages {
    return 1 if $SAVED_PAGES_MIGRATED;
    $pages->migrate_legacy_json_pages;
    $SAVED_PAGES_MIGRATED = 1;
    return 1;
}

sub _collector_jobs {
    return $config->collectors;
}

sub _sync_configured_collector_indicators {
    return 1 if $COLLECTOR_INDICATORS_SYNCED;
    $indicators->sync_collectors( _collector_jobs() );
    $COLLECTOR_INDICATORS_SYNCED = 1;
    return 1;
}

# _runtime_cpanfile_path(%args)
# Resolves the project-local cpanfile used by dashboard cpan installs.
# Input: hash containing an optional path registry under the "paths" key.
# Output: absolute cpanfile path string.
sub _runtime_cpanfile_path {
    my (%args) = @_;
    my $active_paths = $args{paths} || $paths;
    return File::Spec->catfile( $active_paths->runtime_root, 'cpanfile' );
}

# _runtime_local_root(%args)
# Resolves and creates the project-local cpanm installation root.
# Input: hash containing an optional path registry under the "paths" key.
# Output: absolute local-root path string.
sub _runtime_local_root {
    my (%args) = @_;
    my $active_paths = $args{paths} || $paths;
    my $dir = File::Spec->catdir( $active_paths->runtime_root, 'local' );
    if ( !-d $dir ) {
        require File::Path;
        File::Path::make_path($dir);
    }
    if ( $active_paths->can('secure_dir_permissions') ) {
        $active_paths->secure_dir_permissions($dir);
    }
    return $dir;
}

# _runtime_local_lib(%args)
# Resolves the runtime-local Perl library directory exposed to dashboard subprocesses.
# Input: hash containing an optional path registry under the "paths" key.
# Output: absolute local Perl library path string.
sub _runtime_local_lib {
    my (%args) = @_;
    return File::Spec->catdir( _runtime_local_root(%args), 'lib', 'perl5' );
}

# _runtime_perl5lib_env(%args)
# Builds the PERL5LIB environment update for runtime-local optional modules.
# Input: hash containing an optional path registry under the "paths" key.
# Output: hash containing the merged PERL5LIB value.
sub _runtime_perl5lib_env {
    my (%args) = @_;
    my $path_sep = $Config::Config{path_sep} || ':';
    my @perl5lib = grep { defined $_ && $_ ne '' } split /\Q$path_sep\E/, ( $ENV{PERL5LIB} || '' );
    my @local_libs;
    if ( my $paths = $args{paths} ) {
        if ( $paths->can('runtime_local_lib_roots') ) {
            @local_libs = $paths->runtime_local_lib_roots;
        }
        else {
            @local_libs = ( _runtime_local_lib(%args) );
        }
    }
    else {
        @local_libs = ( _runtime_local_lib(%args) );
    }
    for my $local_lib ( reverse @local_libs ) {
        next if !defined $local_lib || $local_lib eq '';
        next if !-d $local_lib;
        next if grep { $_ eq $local_lib } @perl5lib;
        unshift @perl5lib, $local_lib;
    }
    return (
        PERL5LIB => join( $path_sep, @perl5lib ),
    );
}

# _apply_runtime_perl5lib(%args)
# Exposes the runtime-local Perl library to the current dashboard process.
# Input: hash containing an optional path registry under the "paths" key.
# Output: true value.
sub _apply_runtime_perl5lib {
    my (%args) = @_;
    my %env = _runtime_perl5lib_env(%args);
    @ENV{ keys %env } = values %env;
    return 1;
}

# _normalize_runtime_cpan_modules(@modules)
# Validates requested module names, trims whitespace, and removes duplicates while preserving order.
# Input: zero or more requested module-name strings.
# Output: ordered list of validated module-name strings.
sub _normalize_runtime_cpan_modules {
    my (@modules) = @_;
    my @normalized;
    my %seen;
    for my $module (@modules) {
        next if !defined $module;
        $module =~ s/^\s+//;
        $module =~ s/\s+$//;
        next if $module eq '';
        die "Invalid module name '$module'\n" if $module !~ /\A[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z0-9_]+)*\z/;
        next if $seen{$module}++;
        push @normalized, $module;
    }
    return @normalized;
}

# _effective_runtime_cpan_modules(@modules)
# Expands requested module names so any requested DBD driver also installs DBI.
# Input: ordered list of validated requested module-name strings.
# Output: ordered list of module names that should be installed and persisted.
sub _effective_runtime_cpan_modules {
    my (@modules) = @_;
    my @effective = @modules;
    if ( grep { /^DBD::/ } @modules ) {
        unshift @effective, 'DBI' if !grep { $_ eq 'DBI' } @effective;
    }
    my %seen;
    return grep { !$seen{$_}++ } @effective;
}

# _append_runtime_cpan_modules(%args)
# Appends missing module requirements to the runtime cpanfile without duplicating existing entries.
# Input: hash containing an optional path registry under the "paths" key plus a required array reference under "modules".
# Output: absolute cpanfile path string.
sub _append_runtime_cpan_modules {
    my (%args) = @_;
    my $active_paths = $args{paths} || $paths;
    my $modules      = $args{modules};
    die "No modules provided\n" if ref($modules) ne 'ARRAY' || !@{$modules};
    my $cpanfile = _runtime_cpanfile_path( paths => $active_paths );
    my %existing;
    if ( -f $cpanfile ) {
        open my $in, '<', $cpanfile or die "Unable to read $cpanfile: $!";
        while ( my $line = <$in> ) {
            next if $line !~ /requires\s+'([^']+)'/;
            $existing{$1} = 1;
        }
        close $in or die "Unable to close $cpanfile: $!";
    }
    open my $out, '>>', $cpanfile or die "Unable to append $cpanfile: $!";
    for my $module ( @{$modules} ) {
        next if $existing{$module};
        print {$out} "requires '$module';\n";
    }
    close $out or die "Unable to close $cpanfile: $!";
    if ( $active_paths->can('secure_file_permissions') ) {
        $active_paths->secure_file_permissions($cpanfile);
    }
    return $cpanfile;
}

# _install_runtime_cpan_modules(%args)
# Installs optional runtime Perl modules into the project-local local-lib tree and records them in the runtime cpanfile.
# Input: hash containing an optional path registry under the "paths" key and a required array reference of requested modules under "modules".
# Output: result hash reference containing ok/error state, effective module list, paths, and captured command output.
sub _install_runtime_cpan_modules {
    my (%args) = @_;
    my $active_paths = $args{paths} || $paths;
    my $modules      = $args{modules};
    return { error => 'No modules provided' } if ref($modules) ne 'ARRAY' || !@{$modules};

    my @requested = _normalize_runtime_cpan_modules( @{$modules} );
    return { error => 'No valid modules provided' } if !@requested;

    my @install_modules = _effective_runtime_cpan_modules(@requested);
    my $local_root      = _runtime_local_root( paths => $active_paths );

    my ( $stdout, $stderr, $exit ) = capture {
        system( 'cpanm', '-L', $local_root, @install_modules );
        return $? >> 8;
    };
    return {
        error        => "Failed to install runtime Perl modules with cpanm: $stderr",
        stdout       => $stdout,
        stderr       => $stderr,
        requested    => \@requested,
        installed    => \@install_modules,
        runtime_root => $active_paths->runtime_root,
        local_root   => $local_root,
        cpanfile     => _runtime_cpanfile_path( paths => $active_paths ),
    } if $exit != 0;

    _append_runtime_cpan_modules(
        paths   => $active_paths,
        modules => \@install_modules,
    );
    _apply_runtime_perl5lib( paths => $active_paths );

    return {
        ok           => 1,
        stdout       => $stdout,
        stderr       => $stderr,
        requested    => \@requested,
        installed    => \@install_modules,
        runtime_root => $active_paths->runtime_root,
        local_root   => $local_root,
        cpanfile     => _runtime_cpanfile_path( paths => $active_paths ),
    };
}

if ( $cmd eq 'encode' ) {
    local $/;
    my $text = <STDIN>;
    print encode_payload($text), "\n";
    exit 0;
}
elsif ( $cmd eq 'decode' ) {
    local $/;
    my $token = <STDIN>;
    $token =~ s/\s+$//;
    print decode_payload($token);
    exit 0;
}
elsif ( $cmd eq 'indicator' ) {
    _sync_configured_collector_indicators() if ( ( $ARGV[0] || '' ) eq 'list' );
    my $action = shift @ARGV || '';
    if ( $action eq 'set' ) {
        my ( $name, $label, $icon, $status ) = @ARGV;
        die "Usage: dashboard indicator set <name> <label> <icon> <status>\n" if !$status;
        my $item = $indicators->set_indicator(
            $name,
            label          => $label,
            icon           => $icon,
            status         => $status,
            priority       => 100,
            prompt_visible => 1,
        );
        print json_encode($item);
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $indicators->list_indicators ] );
        exit 0;
    }
    elsif ( $action eq 'refresh-core' ) {
        my $cwd = shift @ARGV || cwd();
        print json_encode( $indicators->refresh_core_indicators( cwd => $cwd ) );
        exit 0;
    }
}
elsif ( $cmd eq 'collector' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'write-result' ) {
        my ( $name, $exit_code ) = @ARGV;
        die "Usage: dashboard collector write-result <name> <exit_code>\n" if !defined $exit_code;
        local $/;
        my $stdout = <STDIN>;
        $collectors->write_result(
            $name,
            exit_code => $exit_code,
            stdout    => defined $stdout ? $stdout : '',
            stderr    => '',
        );
        exit 0;
    }
    elsif ( $action eq 'status' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector status <name>\n";
        print json_encode( $collectors->read_status($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $collectors->list_collectors ] );
        exit 0;
    }
    elsif ( $action eq 'job' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector job <name>\n";
        print json_encode( $collectors->read_job($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'output' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector output <name>\n";
        print json_encode( $collectors->read_output($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'inspect' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector inspect <name>\n";
        print json_encode( $collectors->inspect_collector($name) || {} );
        exit 0;
    }
    elsif ( $action eq 'log' ) {
        print $files->read('collector_log') // '';
        exit 0;
    }
    elsif ( $action eq 'run' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector run <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        print json_encode( $runner->run_once($job) );
        exit 0;
    }
    elsif ( $action eq 'start' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector start <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        my $pid = $runner->start_loop($job);
        print "$pid\n";
        exit 0;
    }
    elsif ( $action eq 'stop' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector stop <name>\n";
        my $pid = $runner->stop_loop($name);
        print defined $pid ? "$pid\n" : '';
        exit 0;
    }
    elsif ( $action eq 'restart' ) {
        my $name = shift @ARGV || die "Usage: dashboard collector restart <name>\n";
        my ($job) = grep { $_->{name} eq $name } @{ _collector_jobs() };
        die "Unknown collector '$name'\n" if !$job;
        $runner->stop_loop($name);
        my $pid = $runner->start_loop($job);
        print "$pid\n";
        exit 0;
    }
}
elsif ( $cmd eq 'config' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'init' ) {
        my $file = $config->ensure_global_file;
        print "$file\n";
        exit 0;
    }
    elsif ( $action eq 'show' ) {
        _load_configured_path_aliases();
        print json_encode( $config->merged );
        exit 0;
    }
}
elsif ( $cmd eq 'auth' ) {
    my $action = shift @ARGV || '';
    if ( $action eq 'add-user' ) {
        my ( $username, $password ) = @ARGV;
        die "Usage: dashboard auth add-user <username> <password>\n" if !$username || !$password;
        print json_encode(
            $auth->add_user(
                username => $username,
                password => $password,
            )
        );
        exit 0;
    }
    elsif ( $action eq 'list-users' ) {
        print json_encode( [ $auth->list_users ] );
        exit 0;
    }
    elsif ( $action eq 'remove-user' ) {
        my $username = shift @ARGV || die "Usage: dashboard auth remove-user <username>\n";
        $auth->remove_user($username);
        print json_encode( { removed => $username } );
        exit 0;
    }
}
elsif ( $cmd eq 'init' ) {
    _migrate_saved_pages();
    my $migrated = [];
    my $config_file = $config->ensure_global_file;

    my @pages = $pages->list_saved_pages;
    _seed_init_page( $pages, \@pages, Developer::Dashboard::CLI::SeededPages::api_dashboard_page() );
    _seed_init_page( $pages, \@pages, Developer::Dashboard::CLI::SeededPages::sql_dashboard_page() );
    my $internal_cli = Developer::Dashboard::InternalCLI::ensure_helpers( paths => $paths );

    print json_encode(
        {
            config_file => $config_file,
            internal_cli => $internal_cli,
            runtime_root => $paths->runtime_root,
            migrated_pages => $migrated,
            pages        => [ $pages->list_saved_pages ],
        }
    );
    exit 0;
}
elsif ( $cmd eq 'cpan' ) {
    my @modules = @ARGV;
    my $result = eval {
        _install_runtime_cpan_modules(
            paths   => $paths,
            modules => \@modules,
        );
    };
    if ($@) {
        my $error = $@;
        $error =~ s/\s+\z//;
        die "$error\n";
    }
    die $result->{error} . "\n" if $result->{error};
    print json_encode($result);
    exit 0;
}
elsif ( $cmd eq 'page' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $action = shift @ARGV || '';
    if ( $action eq 'new' ) {
        my $id = shift @ARGV || '';
        my $title = shift @ARGV || 'Untitled';
        my $page = Developer::Dashboard::PageDocument->new(
            id          => $id || undef,
            title       => $title,
            description => 'Project-neutral Developer Dashboard page',
            layout      => {
                body => "Replace this body with your own page content.",
            },
            state => {
                project => '',
            },
            actions => [
                { id => 'example', label => 'Example Action' },
            ],
        );
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'save' ) {
        my $id = shift @ARGV || die "Usage: dashboard page save <id>\n";
        local $/;
        my $source = <STDIN>;
        my $page = $source =~ /^\s*\{/
          ? Developer::Dashboard::PageDocument->from_json($source)
          : Developer::Dashboard::PageDocument->from_instruction($source);
        $page->{id} = $id;
        my $file = $pages->save_page($page);
        print "$file\n";
        exit 0;
    }
    elsif ( $action eq 'list' ) {
        print json_encode( [ $resolver->list_pages ] );
        exit 0;
    }
    elsif ( $action eq 'show' ) {
        my $id = shift @ARGV || die "Usage: dashboard page show <id>\n";
        my $page = $resolver->load_named_page($id);
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'encode' ) {
        my $id = shift @ARGV;
        my $page;
        if ($id) {
            $page = $pages->load_saved_page($id);
        }
        else {
            local $/;
            my $source = <STDIN>;
            $page = $source =~ /^\s*\{/
              ? Developer::Dashboard::PageDocument->from_json($source)
              : Developer::Dashboard::PageDocument->from_instruction($source);
        }
        print $pages->encode_page($page), "\n";
        exit 0;
    }
    elsif ( $action eq 'decode' ) {
        my $token = shift @ARGV || do {
            local $/;
            scalar <STDIN>;
        };
        $token =~ s/\s+$// if defined $token;
        my $page = $pages->load_transient_page($token);
        print $page->canonical_instruction;
        exit 0;
    }
    elsif ( $action eq 'urls' ) {
        my $id = shift @ARGV || die "Usage: dashboard page urls <id>\n";
        my $page = $pages->load_saved_page($id);
        print json_encode(
            {
                edit   => $pages->editable_url($page),
                render => $pages->render_url($page),
                source => $pages->source_url($page),
            }
        );
        exit 0;
    }
    elsif ( $action eq 'render' ) {
        my $source = shift @ARGV || '';
        my $page;
        my $source_kind = 'transient';
        if ($source) {
            if ( -f $source ) {
                open my $fh, '<', $source or die "Unable to read $source: $!";
                local $/;
                my $raw = <$fh>;
                $page = $raw =~ /^\s*\{/
                  ? Developer::Dashboard::PageDocument->from_json($raw)
                  : Developer::Dashboard::PageDocument->from_instruction($raw);
            }
            else {
                $page = $resolver->load_named_page($source);
                $source_kind = $page->{meta}{source_kind} || 'saved';
            }
        }
        else {
            local $/;
            my $source = <STDIN>;
            $page = $source =~ /^\s*\{/
              ? Developer::Dashboard::PageDocument->from_json($source)
              : Developer::Dashboard::PageDocument->from_instruction($source);
        }
        $page = $page_runtime->prepare_page(
            page            => $page,
            source          => $source_kind,
            runtime_context => { params => {} },
        );
        $page->with_mode('render');
        print $page->render_html;
        exit 0;
    }
    elsif ( $action eq 'source' ) {
        my $source = shift @ARGV || die "Usage: dashboard page source <id|token>\n";
        my $page = eval { $resolver->load_named_page($source) };
        if ( !$page ) {
            die $@ if $source !~ /^[A-Za-z0-9+\/=]+$/;
            $page = $pages->load_transient_page($source);
        }
        $page->with_mode('source');
        print $page->canonical_instruction;
        exit 0;
    }
}
elsif ( $cmd eq 'action' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $sub = shift @ARGV || '';
    if ( $sub eq 'run' ) {
        my $page_id = shift @ARGV || die "Usage: dashboard action run <page_id> <action_id>\n";
        my $action_id = shift @ARGV || die "Usage: dashboard action run <page_id> <action_id>\n";
        my $page = $resolver->load_named_page($page_id);
        my ($action) = grep { ref($_) eq 'HASH' && ( $_->{id} || '' ) eq $action_id } @{ $page->as_hash->{actions} || [] };
        die "Unknown action '$action_id'\n" if !$action;
        print json_encode(
            $actions->run_page_action(
                action => $action,
                page   => $page,
                source => $page->{meta}{source_kind} || 'saved',
            )
        );
        exit 0;
    }
}
elsif ( $cmd eq 'docker' ) {
    _load_configured_path_aliases();
    my $sub = shift @ARGV || '';
    if ( $sub eq 'compose' ) {
        my @addons;
        my @modes;
        my @services;
        my $project_root = '';
        my $dry_run = 0;
        Getopt::Long::Configure(qw(pass_through no_getopt_compat no_auto_abbrev));
        GetOptionsFromArray(
            \@ARGV,
            'addon=s@'   => \@addons,
            'mode=s@'    => \@modes,
            'service=s@' => \@services,
            'project=s'  => \$project_root,
            'dry-run!'   => \$dry_run,
        );
        Getopt::Long::Configure(qw(no_pass_through getopt_compat auto_abbrev));
        my $result = $docker->resolve(
            addons       => \@addons,
            args         => \@ARGV,
            modes        => \@modes,
            services     => \@services,
            project_root => $project_root || undef,
        );
        if ($dry_run) {
            print json_encode($result);
            exit 0;
        }
        chdir $result->{project_root} or die "Unable to chdir to $result->{project_root}: $!";
        local @ENV{ keys %{ $result->{env} } } = values %{ $result->{env} } if %{ $result->{env} };
        exec @{ $result->{command} };
        die "Unable to exec docker compose: $!";
    }
}
elsif ( $cmd eq 'serve' ) {
    _load_configured_path_aliases();
    _migrate_saved_pages();
    my $action = @ARGV && $ARGV[0] !~ /^-/ ? shift @ARGV : '';
    if ( $action eq 'logs' ) {
        my $follow = 0;
        my $lines;
        GetOptionsFromArray(
            \@ARGV,
            'f'   => \$follow,
            'n=i' => \$lines,
        );
        print $runtime->web_log(
            follow => $follow,
            ( defined $lines ? ( lines => $lines ) : () ),
        );
        exit 0;
    }
    if ( $action eq 'workers' ) {
        my $workers = shift @ARGV;
        my $host = '0.0.0.0';
        my $port = 7890;
        GetOptionsFromArray(
            \@ARGV,
            'host=s' => \$host,
            'port=i' => \$port,
        );
        my $saved = $config->save_global_web_workers($workers);
        my $running = $runtime->running_web;
        my $pid;
        my $collectors = [];
        if ( !$running ) {
            my $served = $runtime->serve_all(
                host    => $host,
                port    => $port,
                workers => $saved->{workers},
            );
            $pid = $served->{pid};
            $collectors = $served->{collectors} || [];
        }
        print json_encode(
            {
                %{$saved},
                ( defined $pid ? ( pid => $pid ) : () ),
                ( @{$collectors} ? ( collectors => $collectors ) : () ),
            }
        );
        exit 0;
    }
    unshift @ARGV, $action if defined $action && $action ne '';
    my $settings = $config->web_settings;
    my $host = $settings->{host};
    my $port = $settings->{port};
    my $workers = $settings->{workers};
    my $ssl = $settings->{ssl};
    my $foreground = 0;
    GetOptionsFromArray(
        \@ARGV,
        'host=s'      => \$host,
        'port=i'      => \$port,
        'workers=i'   => \$workers,
        'ssl!'        => \$ssl,
        'foreground!' => \$foreground,
    );
    $config->save_global_web_settings(
        host    => $host,
        port    => $port,
        workers => $workers,
        ssl     => $ssl,
    );
    my $result = $runtime->serve_all(
        foreground => $foreground,
        host       => $host,
        port       => $port,
        workers    => $workers,
        ssl        => $ssl,
    );
    if (!$foreground) {
        print json_encode($result);
    }
    exit 0;
}
elsif ( $cmd eq 'stop' ) {
    print json_encode( $runtime->stop_all );
    exit 0;
}
elsif ( $cmd eq 'restart' ) {
    _load_configured_path_aliases();
    my $settings = $config->web_settings;
    my $host = $settings->{host};
    my $port = $settings->{port};
    my $workers = $settings->{workers};
    my $ssl = $settings->{ssl};
    GetOptionsFromArray(
        \@ARGV,
        'host=s'    => \$host,
        'port=i'    => \$port,
        'workers=i' => \$workers,
        'ssl!'      => \$ssl,
    );
    print json_encode(
        $runtime->restart_all(
            host    => $host,
            port    => $port,
            workers => $workers,
            ssl     => $ssl,
        )
    );
    exit 0;
}
elsif ( $cmd eq 'shell' ) {
    my $shell = normalize_shell_name( shift @ARGV || native_shell_name() );
    print _shell_bootstrap($shell);
    exit 0;
}
elsif ( $cmd eq 'doctor' ) {
    my $fix = 0;
    GetOptionsFromArray(
        \@ARGV,
        'fix!' => \$fix,
    );
    print json_encode(
        $doctor->run(
            fix => $fix,
        )
    );
    exit 0;
}
elsif ( $cmd eq 'skills' ) {
    require Developer::Dashboard::SkillManager;
    my $skill_mgr = Developer::Dashboard::SkillManager->new();
    
    my $action = shift @ARGV || '';
    
    if ( $action eq 'install' ) {
        my $git_url = shift @ARGV || '';
        die "Usage: dashboard skills install <git-url>\n" if !$git_url;
        my $result = $skill_mgr->install($git_url);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'uninstall' ) {
        my $repo_name = shift @ARGV || '';
        die "Usage: dashboard skills uninstall <repo-name>\n" if !$repo_name;
        my $result = $skill_mgr->uninstall($repo_name);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'update' ) {
        my $repo_name = shift @ARGV || '';
        die "Usage: dashboard skills update <repo-name>\n" if !$repo_name;
        my $result = $skill_mgr->update($repo_name);
        print json_encode($result);
        exit( $result->{error} ? 1 : 0 );
    }
    elsif ( $action eq 'list' ) {
        my $skills = $skill_mgr->list();
        print json_encode({ skills => $skills });
        exit 0;
    }
    else {
        die "Unknown skills action: $action\nUsage: dashboard skills [install|uninstall|update|list]\n";
    }
}
elsif ( $cmd eq 'skill' ) {
    require Developer::Dashboard::SkillDispatcher;
    my $dispatcher = Developer::Dashboard::SkillDispatcher->new();
    
    my $skill_name = shift @ARGV || '';
    my $skill_cmd = shift @ARGV || '';
    
    die "Usage: dashboard skill <skill-name> <command> [args...]\n" if !$skill_name || !$skill_cmd;
    
    my $result = $dispatcher->dispatch( $skill_name, $skill_cmd, @ARGV );
    
    if ( $result->{error} ) {
        print STDERR $result->{error}, "\n";
        exit 1;
    }
    
    print $result->{stdout} if $result->{stdout};
    print STDERR $result->{stderr} if $result->{stderr};
    exit( $result->{exit_code} || 0 );
}
else {
    die "Unsupported built-in dashboard command '$cmd'\n";
}

# _seed_init_page($pages, $known_ids, $page)
# Saves or refreshes one seeded page when the existing target is still the last
# known dashboard-managed copy, while preserving diverged user-edited pages.
# Input: page store, array reference of known ids, and page document object.
# Output: true when the page already existed or was written.
sub _seed_init_page {
    my ( $pages, $known_ids, $page ) = @_;
    my $id = $page->as_hash->{id} || return 1;
    Developer::Dashboard::CLI::SeededPages::ensure_seeded_page(
        page  => $page,
        pages => $pages,
        paths => $paths,
    );
    push @{ $known_ids || [] }, $id;
    return 1;
}

# _shell_dashboard_command()
# Builds a shell-safe command that re-invokes the current dashboard entrypoint.
# Input: none.
# Output: shell command string suitable for generated shell bootstrap helpers.
sub _shell_dashboard_command {
    my ($shell) = @_;
    my $script = $ENV{DEVELOPER_DASHBOARD_ENTRYPOINT} || File::Spec->rel2abs($0);
    my $repo_lib = $ENV{DEVELOPER_DASHBOARD_REPO_LIB} || File::Spec->rel2abs( File::Spec->catdir( $Bin, '..', 'lib' ) );
    if ( -f File::Spec->catfile( $repo_lib, 'Developer', 'Dashboard.pm' ) ) {
        if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
            return join ' ',
              '&',
              shell_quote_for( $shell, $^X ),
              shell_quote_for( $shell, '-I' . $repo_lib ),
              shell_quote_for( $shell, $script );
        }
        return join ' ',
          shell_quote_for( $shell, $^X ),
          '-I' . shell_quote_for( $shell, $repo_lib ),
          shell_quote_for( $shell, $script );
    }
    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return join ' ',
          '&',
          shell_quote_for( $shell, $^X ),
          shell_quote_for( $shell, $script );
    }
    return join ' ',
      shell_quote_for( $shell, $^X ),
      shell_quote_for( $shell, $script );
}

# _shell_bootstrap($shell)
# Builds the requested shell bootstrap script with shared navigation helpers and
# shell-specific prompt wiring.
# Input: normalized shell name.
# Output: shell bootstrap script string.
sub _shell_bootstrap {
    my ($shell) = @_;
    my $dashboard_cmd = _shell_dashboard_command($shell);
    my $dashboard_json_matches = _shell_json_extract_command(
        $shell,
        'my $h=JSON::XS->new->decode($_); print join qq(\n), @{ $h->{matches} || [] }',
    );
    my $dashboard_json_target = _shell_json_extract_command(
        $shell,
        'my $h=JSON::XS->new->decode($_); print $h->{target} // q{}',
    );
    my $bootstrap = _shell_navigation_bootstrap($shell) . "\n" . _shell_prompt_bootstrap($shell);
    $bootstrap =~ s/__DASHBOARD_CMD__/$dashboard_cmd/g;
    $bootstrap =~ s/__DASHBOARD_JSON_MATCHES__/$dashboard_json_matches/g;
    $bootstrap =~ s/__DASHBOARD_JSON_TARGET__/$dashboard_json_target/g;
    return $bootstrap;
}

# _shell_navigation_bootstrap()
# Builds the portable shell helpers used by all supported interactive shells for
# bookmark-aware navigation.
# Input: none.
# Output: shell function definitions as a script string.
sub _shell_navigation_bootstrap {
    my ($shell) = @_;
    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return <<'POWERSHELL';
function Get-DashboardTarget {
  param([string[]]$DashboardArgs)
  if (-not $DashboardArgs -or $DashboardArgs.Count -eq 0) {
    return [pscustomobject]@{ target = $null; matches = @() }
  }
  $json = __DASHBOARD_CMD__ path cdr @DashboardArgs
  if ($LASTEXITCODE -ne 0 -or -not $json) {
    return [pscustomobject]@{ target = $null; matches = @() }
  }
  $parsed = $json | ConvertFrom-Json
  $matches = @()
  if ($parsed.matches) {
    $matches = @($parsed.matches)
  }
  return [pscustomobject]@{
    target = if ($parsed.target) { [string]$parsed.target } else { $null }
    matches = $matches
  }
}

function cdr {
  param([Parameter(ValueFromRemainingArguments = $true)][string[]]$DashboardArgs)
  $selection = Get-DashboardTarget -DashboardArgs $DashboardArgs
  if ($selection.matches.Count -gt 0) {
    $selection.matches
  }
  if ($selection.target) {
    Set-Location $selection.target
  }
}

Set-Alias dd_cdr cdr

function which_dir {
  param([Parameter(ValueFromRemainingArguments = $true)][string[]]$DashboardArgs)
  $selection = Get-DashboardTarget -DashboardArgs $DashboardArgs
  if ($selection.matches.Count -gt 0) {
    $selection.matches
    return
  }
  if ($selection.target) {
    $selection.target
  }
}
POWERSHELL
    }
    return <<'SH';
cdr() {
  payload="$(__DASHBOARD_CMD__ path cdr "$@")"
  matches="$(printf '%s' "$payload" | __DASHBOARD_JSON_MATCHES__)"
  target="$(printf '%s' "$payload" | __DASHBOARD_JSON_TARGET__)"
  if [ -n "$matches" ]; then
    printf '%s\n' "$matches"
  fi
  if [ -n "$target" ]; then
    cd "$target"
  fi
}

dd_cdr() {
  cdr "$@"
}

which_dir() {
  payload="$(__DASHBOARD_CMD__ path cdr "$@")"
  matches="$(printf '%s' "$payload" | __DASHBOARD_JSON_MATCHES__)"
  target="$(printf '%s' "$payload" | __DASHBOARD_JSON_TARGET__)"
  if [ -n "$matches" ]; then
    printf '%s\n' "$matches"
    return
  fi
  if [ -n "$target" ]; then
    printf '%s\n' "$target"
  fi
}
SH
}

# _shell_json_extract_command($shell, $perl_code)
# Builds the shell-safe JSON decoding command used by generated shell helpers.
# Input: normalized shell name plus one Perl one-liner body string.
# Output: shell command string suitable for use on the right-hand side of a pipe.
sub _shell_json_extract_command {
    my ( $shell, $perl_code ) = @_;
    my $repo_lib = $ENV{DEVELOPER_DASHBOARD_REPO_LIB} || File::Spec->rel2abs( File::Spec->catdir( $Bin, '..', 'lib' ) );
    my @command = ($^X);
    if ( -f File::Spec->catfile( $repo_lib, 'Developer', 'Dashboard.pm' ) ) {
        push @command, '-I' . $repo_lib;
    }
    push @command, '-MJSON::XS', '-0777', '-ne', $perl_code;

    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return join ' ', '&', map { shell_quote_for( $shell, $_ ) } @command;
    }
    return join ' ', map { shell_quote_for( $shell, $_ ) } @command;
}

# _shell_prompt_bootstrap($shell)
# Builds the prompt hook snippet for a supported shell so dashboard ps1 can be
# reused outside bash as well.
# Input: normalized shell name.
# Output: shell snippet string.
sub _shell_prompt_bootstrap {
    my ($shell) = @_;
    if ( $shell eq 'bash' ) {
        return <<'BASH';

export PS1='$(__DASHBOARD_CMD__ ps1 --jobs \j --mode compact)'
BASH
    }
    if ( $shell eq 'zsh' ) {
        return <<'ZSH';

autoload -Uz add-zsh-hook
_dd_update_prompt() {
  PS1="$(__DASHBOARD_CMD__ ps1 --jobs ${#jobstates} --mode compact)"
}
add-zsh-hook precmd _dd_update_prompt
_dd_update_prompt
ZSH
    }
    if ( $shell eq 'sh' ) {
        return <<'SH';

_dd_prompt_command() {
  __DASHBOARD_CMD__ ps1 --mode compact
}
PS1='$(_dd_prompt_command)'
export PS1
SH
    }
    if ( $shell eq 'powershell' || $shell eq 'pwsh' ) {
        return <<'POWERSHELL';
function prompt {
  __DASHBOARD_CMD__ ps1 --mode compact
}
POWERSHELL
    }
    die "Unsupported shell '$shell'\n";
}

__END__

=head1 NAME

_dashboard-core - internal staged built-in command runtime for Developer Dashboard

=head1 SYNOPSIS

  _dashboard-core <built-in-command> [args...]

=head1 DESCRIPTION

This internal helper runs the heavier built-in Developer Dashboard commands
after the public C<dashboard> entrypoint has already handled hook execution and
switchboard dispatch. It is staged under F<~/.developer-dashboard/cli/dd/> as
F<_dashboard-core> and is not meant to be called by users directly. It also
generates the shared shell bootstrap snippets that expose C<cdr>,
C<dd_cdr>, C<which_dir>, and prompt wiring across bash, zsh, POSIX sh, and
PowerShell.

__END__

=pod

=head1 NAME

_dashboard-core - contains the shared private built-in command runtime that the staged helper scripts hand off into.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This private staged runtime owns the heavier built-in command implementations after C<bin/dashboard> has already done hook execution and helper dispatch. It is also where the generated shell bootstrap snippets come from, including C<cdr>, C<which_dir>, and prompt wiring across bash, zsh, POSIX sh, and PowerShell.

=head1 WHY IT EXISTS

It exists so the public entrypoint stays thin while dashboard-managed built-ins still share one internal command runtime. The staged helper wrappers can all hand off into this file instead of each carrying their own copy of command logic.

=head1 WHEN TO USE

Use this file when changing built-in command dispatch after helper staging, shell bootstrap generation, or behavior shared across multiple staged helper wrappers.

=head1 HOW TO USE

Users do not call this file directly. C<dashboard init> or on-demand staging writes wrappers under F<~/.developer-dashboard/cli/dd/>, and those wrappers invoke C<_dashboard-core> with the built-in command name they represent.

=head1 WHAT USES IT

It is used by every staged dashboard-managed helper under F<~/.developer-dashboard/cli/dd/>, by shell bootstrap generation, and by tests that verify the private-helper runtime contract.

=head1 EXAMPLES

Example 1:

  dashboard paths

Run the public built-in command path that stages or re-enters this helper.

Example 2:

  ~/.developer-dashboard/cli/dd/_dashboard-core --help

Inspect the staged helper directly after C<dashboard init> or helper extraction has populated the home runtime.

Example 3:

  prove -lv t/05-cli-smoke.t t/30-dashboard-loader.t

Rerun the focused staged-helper and thin-loader tests after changing helper dispatch behavior.

Example 4:

  prove -lr t

Verify that the helper still behaves correctly inside the complete repository suite.


=for comment FULL-POD-DOC END

=cut
