package Developer::Dashboard::SkillManager;

use strict;
use warnings;

our $VERSION = '2.21';

use Cwd qw(realpath);
use File::Path qw(make_path remove_tree);
use File::Spec;
use File::Basename qw(basename);
use Capture::Tiny qw(capture);
use JSON::XS qw(decode_json encode_json);
use Developer::Dashboard::PathRegistry;

# new()
# Creates a SkillManager instance to handle skill installation, updates, uninstalls.
# Input: none.
# Output: SkillManager object.
sub new {
    my ( $class, %args ) = @_;
    my $paths = $args{paths}
      || Developer::Dashboard::PathRegistry->new(
        home            => ( $ENV{HOME} || (getpwuid($>))[7] || $ENV{USERPROFILE} || die 'Missing home directory' ),
        workspace_roots => [],
        project_roots   => [],
      );

    return bless {
        paths       => $paths,
        skills_root => $paths->skills_root,
    }, $class;
}

# install($git_url)
# Clones a Git repository as a skill into ~/.developer-dashboard/skills/<repo-name>/
# Input: Git URL (can be git@, https://, file:///, etc.)
# Output: hash ref with success status and repo name.
sub install {
    my ( $self, $git_url ) = @_;
    return { error => 'Missing Git URL' } if !$git_url;

    my $repo_name = _extract_repo_name($git_url);
    return { error => "Unable to extract repo name from $git_url" } if !$repo_name;

    my $skill_path = File::Spec->catdir( $self->{skills_root}, $repo_name );
    return { error => "Skill '$repo_name' already installed at $skill_path" } if -d $skill_path;

    $self->{paths}->ensure_dir( $self->{skills_root} );

    my ( $stdout, $stderr, $exit ) = capture {
        system( 'git', 'clone', $git_url, $skill_path );
    };
    if ( $exit != 0 ) {
        remove_tree($skill_path) if -d $skill_path;
        return { error => "Failed to clone $git_url: $stderr" };
    }

    $self->_prepare_skill_layout($skill_path);
    my $dependency = $self->_install_skill_dependencies($skill_path);
    return $dependency if $dependency->{error};

    return {
        success   => 1,
        repo_name => $repo_name,
        path      => $skill_path,
        message   => "Skill '$repo_name' installed successfully",
        metadata  => $self->_skill_metadata( $repo_name, $skill_path ),
    };
}

# uninstall($repo_name)
# Removes a skill completely from ~/.developer-dashboard/skills/<repo-name>/
# Input: skill repo name.
# Output: hash ref with success status.
sub uninstall {
    my ( $self, $repo_name ) = @_;
    
    return { error => 'Missing repo name' } if !$repo_name;
    
    my $skill_path = File::Spec->catdir( $self->{skills_root}, $repo_name );
    return { error => "Skill '$repo_name' not found" } if !-d $skill_path;
    my $real_root = realpath( $self->{skills_root} ) || $self->{skills_root};
    my $real_path = realpath($skill_path) || $skill_path;
    return { error => "Refusing to uninstall path outside skills root: $skill_path" }
      if index( $real_path, $real_root . '/' ) != 0;

    my $error;
    remove_tree( $skill_path, { error => \$error } );
    if ( @$error ) {
        return { error => "Failed to uninstall skill: " . join( ', ', @$error ) };
    }

    return {
        success   => 1,
        repo_name => $repo_name,
        message   => "Skill '$repo_name' uninstalled successfully",
    };
}

# update($repo_name)
# Pulls latest changes from the skill's Git repository.
# Input: skill repo name.
# Output: hash ref with success status.
sub update {
    my ( $self, $repo_name ) = @_;
    
    return { error => 'Missing repo name' } if !$repo_name;
    
    my $skill_path = File::Spec->catdir( $self->{skills_root}, $repo_name );
    return { error => "Skill '$repo_name' not found" } if !-d $skill_path;

    my ( $stdout, $stderr, $exit ) = capture {
        system( 'git', '-C', $skill_path, 'pull', '--ff-only' );
    };
    if ( $exit != 0 ) {
        return { error => "Failed to update skill: $stderr" };
    }

    $self->_prepare_skill_layout($skill_path);
    my $dependency = $self->_install_skill_dependencies($skill_path);
    return $dependency if $dependency->{error};

    return {
        success   => 1,
        repo_name => $repo_name,
        message   => "Skill '$repo_name' updated successfully",
        metadata  => $self->_skill_metadata( $repo_name, $skill_path ),
    };
}

# list()
# Lists all installed skills with metadata.
# Input: none.
# Output: array ref of skill metadata hashes.
sub list {
    my ($self) = @_;
    
    return [] if !-d $self->{skills_root};
    
    opendir( my $dh, $self->{skills_root} ) or return [];
    my @skills;
    
    for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir($dh) ) {
        my $skill_path = File::Spec->catdir( $self->{skills_root}, $entry );
        next unless -d $skill_path;
        
        push @skills, $self->_skill_metadata( $entry, $skill_path );
    }
    
    closedir($dh);
    return \@skills;
}

# get_skill_path($repo_name)
# Returns the full path to an installed skill.
# Input: skill repo name.
# Output: skill path string or undef.
sub get_skill_path {
    my ( $self, $repo_name ) = @_;
    
    return if !$repo_name;
    
    my $skill_path = File::Spec->catdir( $self->{skills_root}, $repo_name );
    return -d $skill_path ? $skill_path : undef;
}

# _extract_repo_name($git_url)
# Extracts repository name from various Git URL formats.
# Input: Git URL string.
# Output: repo name or undef.
sub _extract_repo_name {
    my ($url) = @_;
    
    return if !$url;
    
    # Extract from: git@github.com:user/repo-name.git
    if ( $url =~ m{/([^/]+?)(\.git)?$} ) {
        my $name = $1;
        return $name;
    }
    
    return;
}

# _prepare_skill_layout($skill_path)
# Ensures the isolated skill directory tree exists under one skill root.
# Input: absolute skill root directory path.
# Output: true value.
sub _prepare_skill_layout {
    my ( $self, $skill_path ) = @_;
    for my $dir (
        File::Spec->catdir( $skill_path, 'cli' ),
        File::Spec->catdir( $skill_path, 'config' ),
        File::Spec->catdir( $skill_path, 'config', 'docker' ),
        File::Spec->catdir( $skill_path, 'state' ),
        File::Spec->catdir( $skill_path, 'logs' ),
        File::Spec->catdir( $skill_path, 'local' ),
    ) {
        make_path($dir) if !-d $dir;
        $self->{paths}->secure_dir_permissions($dir);
    }

    my $config_file = File::Spec->catfile( $skill_path, 'config', 'config.json' );
    if ( !-f $config_file ) {
        open my $fh, '>', $config_file or die "Unable to write $config_file: $!";
        print {$fh} "{}\n";
        close $fh;
        $self->{paths}->secure_file_permissions($config_file);
    }
    return 1;
}

# _install_skill_dependencies($skill_path)
# Installs one skill's Perl dependencies into its isolated local library when a cpanfile exists.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_dependencies {
    my ( $self, $skill_path ) = @_;
    my $cpanfile = File::Spec->catfile( $skill_path, 'cpanfile' );
    return { success => 1, skipped => 1 } if !-f $cpanfile;

    my $local_root = File::Spec->catdir( $skill_path, 'local' );
    make_path($local_root) if !-d $local_root;
    $self->{paths}->secure_dir_permissions($local_root);

    my ( $stdout, $stderr, $exit ) = capture {
        system( 'cpanm', '-L', $local_root, '--installdeps', $skill_path );
    };
    return {
        error => "Failed to install skill dependencies for $skill_path: $stderr",
    } if $exit != 0;

    return {
        success => 1,
        stdout  => $stdout,
        stderr  => $stderr,
    };
}

# _skill_metadata($repo_name, $skill_path)
# Summarizes the isolated filesystem and command surface for one installed skill.
# Input: repo name string and absolute skill root directory path.
# Output: metadata hash reference.
sub _skill_metadata {
    my ( $self, $repo_name, $skill_path ) = @_;
    my $cli_root = File::Spec->catdir( $skill_path, 'cli' );
    my @commands;
    if ( -d $cli_root ) {
        opendir( my $dh, $cli_root ) or die "Unable to read $cli_root: $!";
        @commands = sort grep {
            $_ ne '.' && $_ ne '..'
              && -f File::Spec->catfile( $cli_root, $_ )
              && $_ !~ /\.d\z/
        } readdir($dh);
        closedir($dh);
    }

    my @docker_services;
    my $docker_root = File::Spec->catdir( $skill_path, 'config', 'docker' );
    if ( -d $docker_root ) {
        opendir( my $dh, $docker_root ) or die "Unable to read $docker_root: $!";
        @docker_services = sort grep {
            $_ ne '.' && $_ ne '..' && -d File::Spec->catdir( $docker_root, $_ )
        } readdir($dh);
        closedir($dh);
    }

    return {
        name            => $repo_name,
        path            => $skill_path,
        cli_commands    => \@commands,
        has_config      => -f File::Spec->catfile( $skill_path, 'config', 'config.json' ) ? 1 : 0,
        has_cpanfile    => -f File::Spec->catfile( $skill_path, 'cpanfile' ) ? 1 : 0,
        config_root     => File::Spec->catdir( $skill_path, 'config' ),
        docker_root     => $docker_root,
        docker_services => \@docker_services,
        state_root      => File::Spec->catdir( $skill_path, 'state' ),
        logs_root       => File::Spec->catdir( $skill_path, 'logs' ),
        cli_root        => $cli_root,
        local_root      => File::Spec->catdir( $skill_path, 'local' ),
    };
}

1;

__END__

=pod

=head1 NAME

Developer::Dashboard::SkillManager - manage installed dashboard skills

=head1 SYNOPSIS

  use Developer::Dashboard::SkillManager;
  my $manager = Developer::Dashboard::SkillManager->new();
  
  my $result = $manager->install('git@github.com:user/skill-name.git');
  my $list = $manager->list();
  my $path = $manager->get_skill_path('skill-name');
  my $update_result = $manager->update('skill-name');
  my $uninstall_result = $manager->uninstall('skill-name');

=head1 DESCRIPTION

Manages the lifecycle of installed dashboard skills:
- Install: Clone Git repositories as skills
- Uninstall: Remove skills completely
- Update: Pull latest changes from skill repositories
- List: Show all installed skills
- Resolve: Find skill paths and metadata

Skills are isolated under ~/.developer-dashboard/skills/<repo-name>/

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module installs, updates, removes, and lists dashboard skills. It manages the on-disk skill root under F<~/.developer-dashboard/skills/>, clones or updates Git-backed skill repos, prepares the expected directory layout, and helps the rest of the runtime locate installed skills.

=head1 WHY IT EXISTS

It exists because skill lifecycle management is not the same as skill execution. The dashboard needs one module that owns where skills live, how they are installed or refreshed, and how command and bookmark dispatchers find them later.

=head1 WHEN TO USE

Use this file when changing skill install/update/uninstall behavior, the expected skill directory layout, dependency bootstrap rules for skills, or skill listing and lookup semantics.

=head1 HOW TO USE

Construct it with the active paths, then call the install/update/uninstall/list methods from the C<dashboard skills> helper or from tests. Leave command execution and hook handling to C<Developer::Dashboard::SkillDispatcher>.

=head1 WHAT USES IT

It is used by the C<dashboard skills> command family, by the skill dispatcher, by release metadata that documents the isolated skill layout, and by skill lifecycle tests.

=head1 EXAMPLES

Example 1:

  perl -Ilib -MDeveloper::Dashboard::SkillManager -e 1

Do a direct compile-and-load check against the module from a source checkout.

Example 2:

  prove -lv t/19-skill-system.t t/20-skill-web-routes.t

Run the focused regression tests that most directly exercise this module's behavior.

Example 3:

  HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t

Recheck the module under the repository coverage gate rather than relying on a load-only probe.

Example 4:

  prove -lr t

Put any module-level change back through the entire repository suite before release.


=for comment FULL-POD-DOC END

=cut
