#!/usr/bin/env perl
# vim: set ts=8 sts=2 sw=2 tw=100 et ft=perl :
use strict;
use warnings;
use 5.020;
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Path::Tiny;
use Mojo::UserAgent;
use Digest::MD5 'md5_hex';
use Test::File::ShareDir -share => { -dist => { 'OpenAPI-Modern' => 'share' } };
use JSON::Schema::Modern;
use lib 'lib';  # make sure we load the newly-patched version of our modules

# see https://spec.openapis.org/#openapi-specification-schemas for the latest links

# note that these entries are duplicated in JSON::Schema::Modern::Document::OpenAPI
my %files = (
  # metaschema for json schemas contained within openapi documents:
  # standard json schema (presently draft2020-12) + OAD vocabulary
  'oas/dialect/base.schema.json' => 'https://spec.openapis.org/oas/3.1/dialect/2024-11-10',

  # OAD vocabulary definition
  'oas/meta/base.schema.json' => 'https://spec.openapis.org/oas/3.1/meta/2024-11-10',

  # openapi document schema that forces the use of the json schema dialect (no $schema overrides
  # permitted)
  'oas/schema-base.json' => 'https://spec.openapis.org/oas/3.1/schema-base/2025-09-15',

  # the main openapi document schema, with permissive (unvalidated) json schemas
  'oas/schema.json' => 'https://spec.openapis.org/oas/3.1/schema/2025-09-15',

  'oas/LICENSE' => 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/LICENSE',
);

my $web_url = 'https://spec.openapis.org/';

my $ua = Mojo::UserAgent->new(max_redirects => 3);
say "# fetching $web_url" if $ENV{DEBUG};
my $res = $ua->get($web_url)->result;
die "Failed to fetch $web_url", $res->code, " ", $res->message if $res->is_error;

my %new_schemas = ();  # keys are existing OAS schema URIs, defined above in %files

# check the website and find all files that are newer than what we've got
foreach my $e ($res->dom->find('a[href]')->each) {
  my $link = $e->{href};
  next if $link !~ m{^/oas/3.1/[a-z-]+/\d{4}-\d{2}-\d{2}$};

  $link = Mojo::URL->new($web_url)->path($link)->to_string; # normalize slashes

  if (my ($existing) = grep !/github/ && $link =~ m{^\Q${\substr($_, 0, -10)}\E}, values %files) {
    $new_schemas{$existing} //= [];
    push $new_schemas{$existing}->@*, $link if $existing lt $link;
  }
}

# identify outdated schema files, and replace all references to them in the repository
if (my @outdated_schemas = grep $new_schemas{$_}->@*, sort keys %new_schemas) {
  warn join("\n", 'these outdated entries will be replaced everywhere they are found (including in this script itself):',
    map +("$_ -> ".join(', ', sort $new_schemas{$_}->@*)), @outdated_schemas), "\n\n";

  # now we just keep track of the newest version of each..
  %new_schemas = map +($new_schemas{$_}->@* ? ($_ => $new_schemas{$_}->[-1]) : ()), @outdated_schemas;

  foreach my $file (split("\n", `git ls-files`) ) {
    next if $file =~ m{^share/oas/};  # skip references in the files we are replacing
    path($file)->edit_raw(sub {
        foreach my $outdated (@outdated_schemas) {
          s/$outdated/$new_schemas{$outdated}/g;
        }
      });
  }

  # update the values of %files for all schemas that are outdated
  my %inverted_files = reverse %files;
  foreach my $outdated (@outdated_schemas) {
    $inverted_files{$new_schemas{$outdated}} = delete $inverted_files{$outdated};
  }
  %files = reverse %inverted_files;
}
else {
  %new_schemas = ();
}

my $json_decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->utf8(1);
my $js = JSON::Schema::Modern->new(strict => 1, validate_formats => 1);
my %checksums;

# download fresh copies of our files, validate against their schemas and against our code
foreach my $target (sort keys %files) {
  my $uri = $files{$target};

  say "# fetching   $uri -> share/$target" if $ENV{DEBUG};
  my $res = $ua->get($uri)->result;
  die "Failed to fetch $uri", $res->code, " ", $res->message if $res->is_error;

  $target = path('share', $target);
  $target->parent->mkpath;
  $target->spew_raw(my $content = $res->body);
  $checksums{$target} = md5_hex($content);

  next if $target->basename eq 'LICENSE';

  # perform a simple validation, which should use a metaschema that is already preloaded into the
  # JSON::Schema::Modern instance
  my $schema = $json_decoder->decode($content);
  say '# validating ', $schema->{'$id'}, ' -> ', $target if $ENV{DEBUG};
  my $result = $js->validate_schema($schema, { strict => 1 });
  die $result->dump if not $result->valid;
}

# all files must be updated before any of them can be loaded as documents, since they depend on each
# other via the '$schema' and '$vocabulary' keywords... and we delay loading the document class,
# so we get the new versions of our constants
require JSON::Schema::Modern::Document::OpenAPI;

foreach my $target (sort keys %files) {
  $target = path('share', $target);
  next if $target->basename eq 'LICENSE';

  my $schema = $json_decoder->decode($target->slurp_raw);
  say '# loading    ', $schema->{'$id'}, ' -> ', $target if $ENV{DEBUG};

  # Note that if these are new files, then this type of validate won't work...
  # because the files won't be in the resource index. But we can fix this by carefully rearranging
  # the order in which we load the files, and can sequentially add them to the evaluator's resource
  # index.

  my $result = JSON::Schema::Modern::Document->validate(schema => $schema);
  die $result->dump if not $result->valid;

  die 'for uri ', $schema->{'$id'}, ', mismatch between jsonSchemaDialect "',
      $schema->{properties}{jsonSchemaDialect}{default},
      '" and assumed default "',
      JSON::Schema::Modern::Document::OpenAPI->DEFAULT_DIALECT, '"'
    if exists((($schema->{properties}//{})->{jsonSchemaDialect}//{})->{default})
      and $schema->{properties}{jsonSchemaDialect}{default}
        ne JSON::Schema::Modern::Document::OpenAPI->DEFAULT_DIALECT;
}

# compute checksums and record them in the test
path('t/checksums.t')->edit_raw(sub {
  m/^__DATA__$/mg;
  $_ = substr($_, 0, pos()+1).join("\n", map $_.' '.$checksums{$_}, sort keys %checksums)."\n";
});

if (keys %new_schemas) {
  my ($seen_next, $seen_blank);
  say '# edited Changes file' if $ENV{DEBUG};
  path('Changes')->edit_lines_utf8(sub {
    if ($seen_next ||= /^\{\{\$NEXT\}\}/ and not $seen_blank and /^$/) {
      s/^$/          - updated bundled schemas to their latest published versions\n/;
      $seen_blank = 1;
    }
  });

  exec 'git commit -m"update bundled schemas to latest versions" Changes lib share t update-schemas';
}
