package Developer::Dashboard::CLI::SeededPages;

use strict;
use warnings;

our $VERSION = '2.16';

use File::Basename qw(dirname);
use File::Spec;
use File::ShareDir qw(dist_dir);
use Developer::Dashboard::JSON qw(json_decode json_encode);
use Developer::Dashboard::SeedSync ();

use Developer::Dashboard::PageDocument;

my %PAGE_CACHE;
my %ID_TO_ASSET = (
    'api-dashboard' => 'api-dashboard.page',
    'sql-dashboard' => 'sql-dashboard.page',
);
my %LEGACY_MANAGED_PAGE_MD5 = (
    'sql-dashboard' => [
        '7d9101e0e2585c159e575f0dbd49b3ef',
        'f62a03c9ff7d25cdce65ce569cf2e07b',
        '10a14e5749f374a78429654b6c49b5f0',
    ],
);

# api_dashboard_page()
# Loads the seeded api-dashboard bookmark definition from the shipped asset file.
# Input: none.
# Output: Developer::Dashboard::PageDocument object.
sub api_dashboard_page {
    return _page_from_asset('api-dashboard.page');
}

# sql_dashboard_page()
# Loads the seeded sql-dashboard bookmark definition from the shipped asset file.
# Input: none.
# Output: Developer::Dashboard::PageDocument object.
sub sql_dashboard_page {
    return _page_from_asset('sql-dashboard.page');
}

# page_for_id($id)
# Loads one shipped seeded bookmark page by its saved bookmark id.
# Input: seeded bookmark id string such as api-dashboard or sql-dashboard.
# Output: Developer::Dashboard::PageDocument object.
sub page_for_id {
    my ($id) = @_;
    my $filename = _seeded_page_asset_filename($id);
    return _page_from_asset($filename);
}

# seed_manifest_path(%args)
# Resolves the runtime manifest file that records the last shipped md5 written
# for each dashboard-managed seeded page.
# Input: hash containing a required path registry under the "paths" key.
# Output: absolute manifest file path string.
sub seed_manifest_path {
    my (%args) = @_;
    my $paths = $args{paths} || die 'Missing paths registry';
    return File::Spec->catfile( $paths->config_root, 'seeded-pages.json' );
}

# known_managed_page_md5s($id)
# Returns the list of md5 digests that identify shipped dashboard-managed
# copies of one seeded page across current and bridged older releases.
# Input: seeded bookmark id string.
# Output: ordered list of lowercase md5 hex strings.
sub known_managed_page_md5s {
    my ($id) = @_;
    my $filename = _seeded_page_asset_filename($id);
    my %seen;
    my @md5s = (
        Developer::Dashboard::SeedSync::content_md5( _seeded_page_instruction($filename) ),
        @{ $LEGACY_MANAGED_PAGE_MD5{$id} || [] },
    );
    return grep { defined $_ && $_ ne '' && !$seen{$_}++ } @md5s;
}

# is_known_managed_page_md5(%args)
# Decides whether one md5 digest belongs to a shipped dashboard-managed seeded
# page copy that may be refreshed automatically.
# Input: seeded bookmark id string under "id" and md5 hex string under "md5".
# Output: boolean true when the digest belongs to a known dashboard-managed copy.
sub is_known_managed_page_md5 {
    my (%args) = @_;
    my $id  = $args{id}  || '';
    my $md5 = $args{md5} || '';
    return 0 if $id eq '' || $md5 eq '';
    my $matches = scalar grep { $_ eq $md5 } known_managed_page_md5s($id);
    return $matches ? 1 : 0;
}

# ensure_seeded_page(%args)
# Writes or refreshes one shipped seeded page only when the target is missing,
# unchanged from a known dashboard-managed copy, or matches the recorded seed
# manifest md5. Diverged user-edited pages are preserved.
# Input: hash containing required page store, path registry, and page document
# under "pages", "paths", and "page".
# Output: status string: created, updated, current, or preserved.
sub ensure_seeded_page {
    my (%args) = @_;
    my $pages = $args{pages} || die 'Missing page store';
    my $paths = $args{paths} || die 'Missing paths registry';
    my $page  = $args{page}  || die 'Missing seeded page';
    if ( ref($page) ne 'Developer::Dashboard::PageDocument' ) {
        $page = Developer::Dashboard::PageDocument->from_hash($page);
    }

    my $id = $page->as_hash->{id} || die 'Missing seeded page id';
    my $wanted = $page->canonical_instruction;
    my $wanted_md5 = Developer::Dashboard::SeedSync::content_md5($wanted);

    my $current;
    my $loaded = eval {
        $current = $pages->read_saved_entry($id);
        1;
    };
    if ( !$loaded ) {
        die $@ if $@ !~ /Page '\Q$id\E' not found/;
        $pages->save_page($page);
        _record_manifest_md5(
            paths => $paths,
            id    => $id,
            md5   => $wanted_md5,
        );
        return 'created';
    }

    my $current_md5 = Developer::Dashboard::SeedSync::content_md5($current);
    if ( $current_md5 eq $wanted_md5 ) {
        _record_manifest_md5(
            paths => $paths,
            id    => $id,
            md5   => $wanted_md5,
        );
        return 'current';
    }

    if ( _manifest_md5_matches( paths => $paths, id => $id, md5 => $current_md5 )
        || is_known_managed_page_md5( id => $id, md5 => $current_md5 ) )
    {
        $pages->save_page($page);
        _record_manifest_md5(
            paths => $paths,
            id    => $id,
            md5   => $wanted_md5,
        );
        return 'updated';
    }

    return 'preserved';
}

# _page_from_asset($filename)
# Reads one seeded bookmark instruction file and parses it into a page document.
# Input: asset filename under share/seeded-pages.
# Output: Developer::Dashboard::PageDocument object.
sub _page_from_asset {
    my ($filename) = @_;
    die "Missing seeded page filename\n" if !defined $filename || $filename eq '';
    my $instruction = _seeded_page_instruction($filename);
    return Developer::Dashboard::PageDocument->from_instruction($instruction);
}

# _seeded_page_instruction($filename)
# Returns one shipped seeded bookmark instruction string, caching repeated reads.
# Input: asset filename under share/seeded-pages.
# Output: bookmark instruction string.
sub _seeded_page_instruction {
    my ($filename) = @_;
    die "Missing seeded page filename\n" if !defined $filename || $filename eq '';
    return $PAGE_CACHE{$filename} if exists $PAGE_CACHE{$filename};

    my $path = _seeded_page_asset_path($filename);
    open my $fh, '<:raw', $path or die "Unable to read $path: $!";
    my $instruction = do { local $/; <$fh> };
    close $fh or die "Unable to close $path: $!";
    $PAGE_CACHE{$filename} = $instruction;
    return $instruction;
}

# _seeded_page_asset_filename($id)
# Resolves the shipped seeded-page asset filename for one seeded bookmark id.
# Input: seeded bookmark id string.
# Output: asset filename string.
sub _seeded_page_asset_filename {
    my ($id) = @_;
    die "Unknown seeded page id '$id'\n" if !defined $id || !$ID_TO_ASSET{$id};
    return $ID_TO_ASSET{$id};
}

# _seeded_page_asset_path($filename)
# Resolves one shipped seeded bookmark asset path from the repo tree during
# development or from the installed distribution share dir after install.
# Input: asset filename under share/seeded-pages.
# Output: absolute asset path string.
sub _seeded_page_asset_path {
    my ($filename) = @_;
    die "Missing seeded page filename\n" if !defined $filename || $filename eq '';
    my $repo_path = File::Spec->catfile( _repo_seeded_pages_root(), $filename );
    return $repo_path if -f $repo_path;
    return File::Spec->catfile( _shared_seeded_pages_root(), $filename );
}

# _repo_seeded_pages_root()
# Resolves the repo-tree seeded-pages directory for development checkouts.
# Input: none.
# Output: absolute seeded-pages directory path.
sub _repo_seeded_pages_root {
    return File::Spec->catdir(
        dirname(__FILE__),
        File::Spec->updir,
        File::Spec->updir,
        File::Spec->updir,
        File::Spec->updir,
        'share',
        'seeded-pages',
    );
}

# _shared_seeded_pages_root()
# Resolves the installed distribution share directory for seeded bookmark assets.
# Input: none.
# Output: absolute seeded-pages directory path inside the installed dist share.
sub _shared_seeded_pages_root {
    return File::Spec->catdir( dist_dir('Developer-Dashboard'), 'seeded-pages' );
}

# _read_manifest(%args)
# Loads the runtime seeded-page manifest when present.
# Input: hash containing a required path registry under the "paths" key.
# Output: hash reference of seeded-page manifest entries.
sub _read_manifest {
    my (%args) = @_;
    my $manifest_path = seed_manifest_path(%args);
    return {} if !-f $manifest_path;
    open my $fh, '<:raw', $manifest_path or die "Unable to read $manifest_path: $!";
    my $json = do { local $/; <$fh> };
    close $fh or die "Unable to close $manifest_path: $!";
    $json = '{}' if !defined $json || $json =~ /\A\s*\z/;
    my $manifest = json_decode($json);
    die "Seed manifest at $manifest_path must decode to a hash\n" if ref($manifest) ne 'HASH';
    return $manifest;
}

# _write_manifest(%args)
# Persists one seeded-page manifest hash to the runtime config tree.
# Input: hash containing a required path registry under "paths" and a manifest
# hash reference under "manifest".
# Output: manifest file path string.
sub _write_manifest {
    my (%args) = @_;
    my $paths = $args{paths} || die 'Missing paths registry';
    my $manifest = $args{manifest};
    die 'Missing seeded page manifest hash' if ref($manifest) ne 'HASH';
    my $manifest_path = seed_manifest_path( paths => $paths );
    open my $fh, '>:raw', $manifest_path or die "Unable to write $manifest_path: $!";
    print {$fh} json_encode($manifest);
    print {$fh} "\n";
    close $fh or die "Unable to close $manifest_path: $!";
    $paths->secure_file_permissions($manifest_path) if $paths->can('secure_file_permissions');
    return $manifest_path;
}

# _record_manifest_md5(%args)
# Records the latest shipped md5 for one seeded page in the runtime manifest.
# Input: hash containing required path registry, seeded page id, and md5 string
# under "paths", "id", and "md5".
# Output: recorded md5 string.
sub _record_manifest_md5 {
    my (%args) = @_;
    my $paths = $args{paths} || die 'Missing paths registry';
    my $id    = $args{id}    || die 'Missing seeded page id';
    my $md5   = $args{md5}   || die 'Missing seeded page md5';
    my $manifest = _read_manifest( paths => $paths );
    $manifest->{$id} = {
        asset => _seeded_page_asset_filename($id),
        md5   => $md5,
    };
    _write_manifest(
        paths    => $paths,
        manifest => $manifest,
    );
    return $md5;
}

# _manifest_md5_matches(%args)
# Checks whether the runtime manifest recorded one seeded page at the supplied
# md5 digest, proving the current file is still the last dashboard-managed copy.
# Input: hash containing required path registry, seeded page id, and md5 string.
# Output: boolean true when the manifest matches the supplied md5.
sub _manifest_md5_matches {
    my (%args) = @_;
    my $paths = $args{paths} || die 'Missing paths registry';
    my $id    = $args{id}    || '';
    my $md5   = $args{md5}   || '';
    return 0 if $id eq '' || $md5 eq '';
    my $manifest = _read_manifest( paths => $paths );
    my $recorded = $manifest->{$id}{md5} || '';
    return $recorded eq $md5 ? 1 : 0;
}

1;

__END__

=head1 NAME

Developer::Dashboard::CLI::SeededPages - shipped bookmark assets for dashboard init

=head1 SYNOPSIS

  use Developer::Dashboard::CLI::SeededPages;
  my $page = Developer::Dashboard::CLI::SeededPages::api_dashboard_page();

=head1 DESCRIPTION

Loads the shipped seeded bookmark instruction files used by C<dashboard init>
and runtime bootstrap so the public C<dashboard> entrypoint does not need to
embed the full bookmark source for the API and SQL dashboards. It also owns
the non-destructive starter-page refresh contract: missing or stale
dashboard-managed copies may refresh to the latest shipped seed, while
diverged user-edited saved pages stay untouched.

=for comment FULL-POD-DOC START

=head1 PURPOSE

Perl module in the Developer Dashboard codebase. This file loads shipped bookmark source files from the distribution share directory.
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 to keep this responsibility in reusable Perl code instead of hiding it in the thin C<dashboard> switchboard, bookmark text, or duplicated helper scripts. That separation makes the runtime easier to test, safer to change, and easier for contributors to navigate.

=head1 WHEN TO USE

Use this file when you are changing the underlying runtime behaviour it owns, when you need to call its routines from another part of the project, or when a failing test points at this module as the real owner of the bug.

=head1 HOW TO USE

Load C<Developer::Dashboard::CLI::SeededPages> from Perl code under C<lib/> or from a focused test, then use the public routines documented in the inline function comments and existing SYNOPSIS/METHODS sections. This file is not a standalone executable.

=head1 WHAT USES IT

This file is used by whichever runtime path owns this responsibility: the public C<dashboard> entrypoint, staged private helper scripts under C<share/private-cli/>, the web runtime, update flows, and the focused regression tests under C<t/>.

=head1 EXAMPLES

  perl -Ilib -MDeveloper::Dashboard::CLI::SeededPages -e 'print qq{loaded\n}'

That example is only a quick load check. For real usage, follow the public routines already described in the inline code comments and any existing SYNOPSIS section.

=for comment FULL-POD-DOC END

=cut
