#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

BEGIN {
    require lib;
    my $repo_lib = $ENV{DEVELOPER_DASHBOARD_REPO_LIB};
    if ( defined $repo_lib && $repo_lib ne '' && -d $repo_lib ) {
        lib->import($repo_lib);
    }
    else {
        require FindBin;
        die 'FindBin did not resolve a helper directory' if !defined $FindBin::Bin || $FindBin::Bin eq '';
        lib->import("$FindBin::Bin/../lib");
    }
}

use Developer::Dashboard::Config;
use Developer::Dashboard::IndicatorStore;
use Developer::Dashboard::PathRegistry;
use Developer::Dashboard::Prompt;
use Developer::Dashboard::PathIdentity qw(current_working_directory);

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

if ( !defined $ENV{HOME} || $ENV{HOME} eq '' ) {
    if ( defined $ENV{USERPROFILE} && $ENV{USERPROFILE} ne '' ) {
        $ENV{HOME} = $ENV{USERPROFILE};
    }
    elsif (
        defined $ENV{HOMEDRIVE} && $ENV{HOMEDRIVE} ne ''
        && defined $ENV{HOMEPATH} && $ENV{HOMEPATH} ne ''
      )
    {
        require File::Spec;
        $ENV{HOME} = File::Spec->catdir( $ENV{HOMEDRIVE}, $ENV{HOMEPATH} );
    }
}

# _default_roots()
# Resolves the standard workspace roots used by the lightweight prompt helper.
# Input: none.
# Output: ordered list of existing directory path strings.
sub _default_roots {
    my $home = $ENV{HOME} || '';
    return grep { defined && -d } map { "$home/$_" } qw(projects src work);
}

# _tmux_ticket_ref()
# Reads TICKET_REF from the active tmux session environment when available.
# Input: none.
# Output: ticket string or undef when tmux does not expose one.
sub _tmux_ticket_ref {
    return if !defined $ENV{TMUX} || $ENV{TMUX} eq '';
    require Capture::Tiny;
    my ( $stdout, undef, $exit_code ) = Capture::Tiny::capture( sub {
        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;
}

# _prime_ticket_ref_env()
# Seeds TICKET_REF from tmux when the shell has not already exported 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;
}

# _collectors_need_sync($indicator_store, $collector_jobs)
# Determines whether collector-managed prompt indicators need a sync write.
# Input: indicator store object and collector job array reference.
# Output: boolean true when the helper should run sync_collectors.
sub _collectors_need_sync {
    my ( $indicator_store, $collector_jobs ) = @_;
    return 1 if !$indicator_store;
    return 1 if !$indicator_store->can('collectors_need_sync');
    return $indicator_store->collectors_need_sync($collector_jobs) ? 1 : 0;
}

# _parse_ps1_options_fast($args, %targets)
# Parses the exact option spellings emitted by the generated shell prompt
# bootstrap without initializing Getopt::Long; unsupported syntax is left
# untouched for the compatibility parser.
# Input: mutable argv array reference and option-name-to-scalar-reference map.
# Output: true when every argument was parsed, false when compatibility parsing is required.
sub _parse_ps1_options_fast {
    my ( $args, %targets ) = @_;
    return 0 if ref($args) ne 'ARRAY';

    my %parsed;
    my %string_options = map { $_ => 1 } qw(jobs cwd mode max-age width);
    my %boolean_options = (
        '--color'           => [ color          => 1 ],
        '--no-color'        => [ color          => 0 ],
        '--no-indicators'   => [ no_indicators  => 1 ],
        '--no-no-indicators'=> [ no_indicators  => 0 ],
    );

    my @remaining = @{$args};
    while (@remaining) {
        my $option = shift @remaining;
        if ( exists $boolean_options{$option} ) {
            my ( $name, $value ) = @{ $boolean_options{$option} };
            $parsed{$name} = $value;
            next;
        }

        if ( $option =~ /^--([a-z][a-z-]*)=(.*)$/s && $string_options{$1} ) {
            my ( $name, $value ) = ( $1, $2 );
            if ( $name eq 'jobs' || $name eq 'max-age' || $name eq 'width' ) {
                return 0 if $value !~ /^[+-]?\d+$/;
            }
            $parsed{$name} = $value;
            next;
        }

        if ( $option =~ /^--([a-z][a-z-]*)$/ && $string_options{$1} ) {
            return 0 if !@remaining;
            my ( $name, $value ) = ( $1, shift @remaining );
            if ( $name eq 'jobs' || $name eq 'max-age' || $name eq 'width' ) {
                return 0 if $value !~ /^[+-]?\d+$/;
            }
            $parsed{$name} = $value;
            next;
        }

        return 0;
    }

    for my $name ( keys %parsed ) {
        return 0 if ref( $targets{$name} ) ne 'SCALAR';
    }
    ${ $targets{$_} } = $parsed{$_} for keys %parsed;
    @{$args} = ();
    return 1;
}

# main(@ARGV)
# Runs the prompt renderer helper for Developer Dashboard.
# Input: command-line arguments from @ARGV.
# Output: prints one prompt string suitable for shell command substitution.
my $jobs = 0;
my $cwd  = current_working_directory();
my $mode = 'compact';
my $color = 0;
my $max_age = 300;
my $no_indicators = 0;
my $width = 0;
if ( !_parse_ps1_options_fast(
        \@ARGV,
        jobs          => \$jobs,
        cwd           => \$cwd,
        mode          => \$mode,
        color         => \$color,
        'max-age'     => \$max_age,
        width         => \$width,
        no_indicators => \$no_indicators,
    )
  )
{
    require Getopt::Long;
    Getopt::Long::GetOptionsFromArray(
        \@ARGV,
        'jobs=i'         => \$jobs,
        'cwd=s'          => \$cwd,
        'mode=s'         => \$mode,
        'color!'         => \$color,
        'max-age=i'      => \$max_age,
        'width=i'        => \$width,
        'no-indicators!' => \$no_indicators,
    );
}

my @roots = _default_roots();
my $paths = Developer::Dashboard::PathRegistry->new(
    home            => ( $ENV{HOME} || '' ),
    cwd             => $cwd,
    workspace_roots => \@roots,
    project_roots   => \@roots,
);
my $config = Developer::Dashboard::Config->for_path_config($paths);
my $indicators = Developer::Dashboard::IndicatorStore->new( paths => $paths );
my $prompt = Developer::Dashboard::Prompt->new(
    indicators => $indicators,
    paths      => $paths,
);

my $collector_jobs = $config->collectors;
$indicators->sync_collectors($collector_jobs)
  if _collectors_need_sync( $indicators, $collector_jobs );
$indicators->refresh_core_indicators( cwd => $cwd, prompt_only => 1 );
_prime_ticket_ref_env();

if ( $mode eq 'tmux-status' || $mode eq 'tmux-status-top' || $mode eq 'tmux-status-bottom' ) {
    my $line = $mode eq 'tmux-status-bottom' ? 'bottom' : 'top';
    $line = '' if $mode eq 'tmux-status';
    print $prompt->render_tmux_status(
        color   => $color,
        max_age => $max_age,
        width   => $width,
        ( $line ne '' ? ( line => $line ) : () ),
    );
}
else {
    print $prompt->render(
        jobs          => $jobs,
        cwd           => $cwd,
        mode          => $mode,
        color         => $color,
        max_age       => $max_age,
        no_indicators => $no_indicators,
    );
}

__END__

=pod

=head1 NAME

ps1 - private prompt helper for Developer Dashboard

=head1 SYNOPSIS

  dashboard ps1 [--jobs N] [--cwd DIR] [--mode compact|extended] [--color] [--max-age N]

=head1 DESCRIPTION

This private helper is staged under F<~/.developer-dashboard/cli/dd/> so the main
C<dashboard> command can keep prompt rendering on a lightweight handoff path.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This staged helper exposes C<dashboard ps1>, the prompt-rendering command used by generated shell bootstraps. It is the CLI face of the prompt subsystem.

=head1 WHY IT EXISTS

It exists because prompt rendering is built into the product, but the prompt text should come from the prompt module rather than from shell code or the public switchboard.

=head1 WHEN TO USE

Use this file when changing prompt CLI flags, compact versus extended mode handling, or the handoff from the helper into the prompt renderer.

=head1 HOW TO USE

Users or shell bootstrap functions run C<dashboard ps1 ...>. The staged helper
uses C<DEVELOPER_DASHBOARD_REPO_LIB> from the public switchboard when that
directory is available, avoiding a C<FindBin> startup import. Direct standalone
invocation retains a C<FindBin> fallback to the helper-relative library
directory. The helper forwards the request into the private runtime, which
loads indicator and collector state and prints the rendered prompt fragment.
Exact options emitted by generated prompts are parsed with a small local parser;
unsupported or abbreviated forms continue through C<Getopt::Long> so its
compatibility behavior remains available when needed.

=head1 WHAT USES IT

It is used by generated bash/zsh/sh/PowerShell prompts, by prompt smoke tests, and by contributors checking prompt output while iterating on indicator behavior.

=head1 EXAMPLES

Example 1:

  dashboard shell ps1

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

Example 2:

  ~/.developer-dashboard/cli/dd/ps1 --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
