#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

use Config ();
use Cwd qw(abs_path);
use FindBin qw($Bin);
use lib "$Bin/../lib";
use File::Spec;
use IO::Handle ();
use IO::Select ();
use IPC::Open3 qw(open3);
use Pod::Usage qw(pod2usage);
use Symbol qw(gensym);

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

use Developer::Dashboard::Platform qw(
  command_argv_for_path
  is_runnable_file
  resolve_runnable_file
);
use Developer::Dashboard::InternalCLI ();
use Developer::Dashboard::Runtime::Result ();

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 || '';
_prime_command_result_env( $cmd, @ARGV ) if $cmd ne '';

if ( $cmd eq '' ) {
    pod2usage(
        -exitval  => 1,
        -verbose  => 99,
        -sections => [qw(NAME SYNOPSIS)],
    );
}
elsif ( $cmd eq 'help' || $cmd eq '--help' || $cmd eq '-h' ) {
    pod2usage(
        -exitval => 0,
        -verbose => 99,
    );
}

if ( $cmd eq 'version' ) {
    require Developer::Dashboard;
    print $Developer::Dashboard::VERSION, "\n";
    exit 0;
}

if ( my $helper_path = _builtin_helper_path($cmd) ) {
    _exec_switchboard_command( $helper_path, @ARGV );
}

if ( my $custom_path = _custom_command_path($cmd) ) {
    _exec_switchboard_command( $custom_path, @ARGV );
}

pod2usage(
    -exitval  => 1,
    -verbose  => 99,
    -sections => [qw(NAME SYNOPSIS)],
);

# _prime_command_result_env($cmd, @argv)
# Runs executable per-command hook files from every DD-OOP-LAYERS CLI root and
# exposes their captured output through RESULT JSON for later hooks and the
# final command target.
# Input: top-level command name plus the remaining argv list.
# Output: true value after hook execution completes.
sub _prime_command_result_env {
    my ( $cmd, @argv ) = @_;
    Developer::Dashboard::Runtime::Result::clear_current();
    $ENV{DEVELOPER_DASHBOARD_COMMAND} = $cmd if defined $cmd && $cmd ne '';
    return 1 if !defined $cmd || $cmd eq '';

    my @hook_roots = _command_hook_roots($cmd);
    return 1 if !@hook_roots;
    my $stream = $cmd eq 'doctor' ? 0 : 1;

    my %results;
    for my $hook_root (@hook_roots) {
        opendir my $dh, $hook_root or next;
        for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir $dh ) {
            my $path = File::Spec->catfile( $hook_root, $entry );
            next if !is_runnable_file($path);
            next if $entry eq 'run';

            my $hook_result = _run_command_hook_streaming(
                $path,
                stream => $stream,
                argv   => \@argv,
            );
            my $result_key = exists $results{$entry} ? _command_hook_result_key($path) : $entry;
            $results{$result_key} = {
                stdout => $hook_result->{stdout},
                stderr => $hook_result->{stderr},
            };
            $results{$result_key}{exit_code} = $hook_result->{exit_code} if defined $hook_result->{exit_code};
            Developer::Dashboard::Runtime::Result::set_current( \%results );
        }
        closedir $dh;
    }

    Developer::Dashboard::Runtime::Result::clear_current() if !%results;
    return 1;
}

# _run_command_hook_streaming($path, %args)
# Executes one hook file, streams its output live, and captures stdout/stderr so
# later hooks and the final command can inspect RESULT JSON.
# Input: executable hook path plus an optional stream flag and argv array ref.
# Output: hash reference containing stdout, stderr, and exit_code.
sub _run_command_hook_streaming {
    my ( $path, %args ) = @_;
    my $stream = exists $args{stream} ? $args{stream} : 1;
    my @argv = @{ $args{argv} || [] };
    open my $stdin, '<', File::Spec->devnull() or die "Unable to open " . File::Spec->devnull() . " for hook stdin: $!";
    my $stderr = gensym();
    my $stdout;
    my @command = command_argv_for_path($path);
    my $pid = open3( $stdin, $stdout, $stderr, @command, @argv );
    close $stdin;

    my $selector  = IO::Select->new( $stdout, $stderr );
    my $stdout_fd = fileno($stdout);
    my $stderr_fd = fileno($stderr);
    my $stdout_text = '';
    my $stderr_text = '';
    local $| = 1;
    STDOUT->autoflush(1);
    STDERR->autoflush(1);

    while ( my @ready = $selector->can_read ) {
        for my $fh (@ready) {
            my $buffer = '';
            my $read = sysread( $fh, $buffer, 8192 );
            if ( !defined $read || $read == 0 ) {
                $selector->remove($fh);
                close $fh;
                next;
            }

            if ( fileno($fh) == $stdout_fd ) {
                print STDOUT $buffer if $stream;
                $stdout_text .= $buffer;
                next;
            }

            if ( fileno($fh) == $stderr_fd ) {
                print STDERR $buffer if $stream;
                $stderr_text .= $buffer;
                next;
            }
        }
    }

    waitpid( $pid, 0 );
    return {
        stdout    => $stdout_text,
        stderr    => $stderr_text,
        exit_code => $? >> 8,
    };
}

# _command_hook_roots($cmd)
# Resolves every per-command hook directory across DD-OOP-LAYERS from home to
# the deepest participating layer.
# Input: top-level command name string.
# Output: ordered list of hook directory paths.
sub _command_hook_roots {
    my ($cmd) = @_;
    return () if !defined $cmd || $cmd eq '';
    my @roots;
    for my $root ( _cli_runtime_layers() ) {
        my $plain_root = File::Spec->catdir( $root, $cmd );
        if ( -d $plain_root ) {
            push @roots, $plain_root;
            next;
        }
        my $d_root = File::Spec->catdir( $root, $cmd . '.d' );
        push @roots, $d_root if -d $d_root;
    }
    return @roots;
}

# _custom_command_path($cmd)
# Resolves the effective layered custom command target from the deepest child
# layer outward to the home runtime.
# Input: top-level command name string.
# Output: file or directory path string, or empty string when absent.
sub _custom_command_path {
    my ($cmd) = @_;
    return '' if !defined $cmd || $cmd eq '';
    for my $root ( _cli_runtime_roots() ) {
        my $path = File::Spec->catfile( $root, $cmd );
        return $path if -e $path;
    }
    return '';
}

# _builtin_helper_path($cmd)
# Resolves one dashboard-managed built-in helper under the home runtime and
# stages the shipped helper assets there on demand.
# Input: top-level command name string.
# Output: helper file path string or empty string when unsupported.
sub _builtin_helper_path {
    my ($cmd) = @_;
    my $helper = Developer::Dashboard::InternalCLI::canonical_helper_name($cmd);
    return '' if $helper eq '';
    require Developer::Dashboard::PathRegistry;
    my $paths = Developer::Dashboard::PathRegistry->new(
        home            => $ENV{HOME},
        workspace_roots => [],
        project_roots   => [],
    );
    Developer::Dashboard::InternalCLI::ensure_helpers( paths => $paths );
    return Developer::Dashboard::InternalCLI::helper_path( paths => $paths, name => $helper );
}

# _exec_switchboard_command($path, @argv)
# Replaces the current dashboard process with a resolved built-in helper or
# layered custom command target.
# Input: resolved file or directory path plus argv list for the target command.
# Output: never returns on success; false when the target is not runnable.
sub _exec_switchboard_command {
    my ( $path, @argv ) = @_;
    return 0 if !defined $path || $path eq '';
    $ENV{DEVELOPER_DASHBOARD_ENTRYPOINT} ||= abs_path($0) || $0;
    $ENV{DEVELOPER_DASHBOARD_REPO_LIB} ||= $dashboard_lib if defined $dashboard_lib && -d $dashboard_lib;

    if ( -d $path ) {
        my $run = _resolve_directory_runner($path);
        if ($run) {
            my @command = command_argv_for_path($run);
            exec { $command[0] } @command, @argv;
            die "Unable to exec $run: $!";
        }
    }
    if ( my $command_path = resolve_runnable_file($path) ) {
        my @command = command_argv_for_path($command_path);
        exec { $command[0] } @command, @argv;
        die "Unable to exec $command_path: $!";
    }
    return 0;
}

# _command_hook_result_key($path)
# Builds a stable RESULT hash key for one hook path so same-named hooks from
# different layers do not overwrite each other.
# Input: absolute hook path string.
# Output: stable hook result key string.
sub _command_hook_result_key {
    my ($path) = @_;
    my $home = $ENV{HOME} || '';
    return File::Spec->abs2rel( $path, $home ) if $home ne '';
    return $path;
}

# _cli_runtime_layers()
# Resolves the participating CLI roots for DD-OOP-LAYERS in home-to-leaf order.
# Input: none.
# Output: ordered list of CLI root directory paths.
sub _cli_runtime_layers {
    require Developer::Dashboard::PathRegistry;
    my $paths = Developer::Dashboard::PathRegistry->new(
        home            => $ENV{HOME},
        workspace_roots => [],
        project_roots   => [],
    );
    return $paths->cli_layers;
}

# _cli_runtime_roots()
# Returns layered CLI roots in deepest-first lookup order for top-level command
# resolution.
# Input: none.
# Output: ordered list of CLI root directory paths.
sub _cli_runtime_roots {
    return reverse _cli_runtime_layers();
}

# _resolve_directory_runner($dir)
# Resolves the runnable entrypoint for one directory-backed command or helper.
# Input: command directory path.
# Output: runnable file path string or undef when no known runner exists.
sub _resolve_directory_runner {
    my ($dir) = @_;
    return if !defined $dir || $dir eq '' || !-d $dir;
    for my $name (qw(run run.pl run.ps1 run.cmd run.bat run.sh run.bash)) {
        my $path = File::Spec->catfile( $dir, $name );
        my $resolved = resolve_runnable_file($path);
        return $resolved if $resolved;
    }
    return;
}

__END__

=head1 NAME

dashboard - thin command switchboard for Developer Dashboard

=head1 SYNOPSIS

  dashboard help
  dashboard init
  dashboard update
  dashboard doctor [--fix]
  dashboard ps1 [--jobs N] [--cwd PATH] [--mode compact|extended] [--color]
  dashboard paths
  dashboard path list
  dashboard path resolve <name>
  dashboard path add <name> <path>
  dashboard path del <name>
  dashboard path locate <term...>
  dashboard path project-root
  dashboard of [--print] [--line N] [--editor CMD] <file|scope> [pattern...]
  dashboard open-file [--print] [--line N] [--editor CMD] <file|scope> [pattern...]
  dashboard ticket [ticket-ref]
  dashboard jq [path] [file]
  dashboard yq [path] [file]
  dashboard tomq [path] [file]
  dashboard propq [path] [file]
  dashboard iniq [path] [file]
  dashboard csvq [path] [file]
  dashboard xmlq [path] [file]
  dashboard encode < input.txt
  dashboard decode < token.txt
  dashboard indicator set <name> <label> <icon> <status>
  dashboard indicator list
  dashboard indicator refresh-core [cwd]
  dashboard collector write-result <name> <exit_code>
  dashboard collector status <name>
  dashboard collector list
  dashboard collector job <name>
  dashboard collector output <name>
  dashboard collector inspect <name>
  dashboard collector log
  dashboard collector run <name>
  dashboard collector start <name>
  dashboard collector stop <name>
  dashboard collector restart <name>
  dashboard skills install <git-url>
  dashboard skills uninstall <repo-name>
  dashboard skills update <repo-name>
  dashboard skills list
  dashboard skill <repo-name> <command> [args...]
  dashboard config init
  dashboard config show
  dashboard auth add-user <username> <password>
  dashboard auth list-users
  dashboard auth remove-user <username>
  dashboard page new [id] [title]
  dashboard page save <id>
  dashboard page list
  dashboard page show <id>
  dashboard page encode [id]
  dashboard page decode [token]
  dashboard page urls <id>
  dashboard page render [id|file]
  dashboard page source <id|token>
  dashboard action run <page_id> <action_id>
  dashboard docker compose [--addon NAME] [--mode NAME] [--service NAME] [--project DIR] [--dry-run] <compose-args...>
  dashboard serve [logs [-f] [-n N]|workers <N>] [--host HOST] [--port PORT] [--workers N] [--foreground]
  dashboard stop
  dashboard restart [--host HOST] [--port PORT] [--workers N]
  dashboard shell [bash|zsh|sh|ps|powershell|pwsh]
  dashboard version
  dashboard <custom-subcommand> [args...]

=head1 DESCRIPTION

The public C<dashboard> entrypoint is intentionally kept thin.

It only:

=over 4

=item *

bootstraps the repo lib path when running from a checkout

=item *

runs layered per-command hooks across C<DD-OOP-LAYERS>

=item *

stages dashboard-managed built-in helpers under F<~/.developer-dashboard/cli/dd/>

=item *

resolves the effective command from the deepest child layer back to home

=item *

execs the resolved helper or custom command

=back

The real built-in command implementations live outside this entrypoint, either
in private staged helper scripts under F<share/private-cli/> or in reusable
Perl modules loaded by those helpers.

=for comment FULL-POD-DOC START

=head1 PURPOSE

Public entrypoint script in the Developer Dashboard codebase. This file bootstraps the runtime, resolves DD-OOP-LAYERS hooks and command targets, stages private helpers, and hands execution off to the final command.
Open this file when you need the implementation, regression coverage, or runtime entrypoint for that responsibility rather than guessing which part of the tree owns it.

=head1 WHY IT EXISTS

It exists as the one public executable users run. Its job is intentionally narrow: bootstrap the environment, evaluate layered hooks, resolve the final command, and hand off without owning the heavy implementation bodies itself.

=head1 WHEN TO USE

Use this file when you are changing top-level command bootstrap, layered hook execution, helper staging, or command handoff behaviour.

=head1 HOW TO USE

Run C<dashboard E<lt>commandE<gt>>. If you are changing this file, keep it focused on bootstrap, layered hook execution, helper staging, command resolution, and C<exec>; move heavy behaviour into modules or staged helper scripts instead.

=head1 WHAT USES IT

It is used directly by end users, by tests that execute C<dashboard ...>, by staged shell helpers that re-enter the public command, and by release/integration smoke flows.

=head1 EXAMPLES

  dashboard paths
  dashboard page list

Both commands go through this public switchboard, which then resolves hooks and hands off to the real helper implementation.

=for comment FULL-POD-DOC END

=cut
