#!/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 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
# these are updated automatically at build time via 'update-schemas'

# 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/oas';

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;

# keys are existing OAS schema URLs, as found above
# values is the list of all schemas found on the website of the same type
my %new_schemas;

# check the website and find all files that are newer than what we've got
my $links;
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}$};

  ++$links;
  $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;
  }
}

die "no links found on $web_url?!" if not $links;

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

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

  if ($ENV{DEBUG}) {
    foreach my $outdated (@outdated_schemas) {
      say 'replacing "'.$outdated.'" with "'.$new_schemas{$outdated}.'"';
    }
  }

  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
  # we want to download even unchanged files to make sure the checksum matches
  my %inverted_files = reverse %files;
  foreach my $outdated (@outdated_schemas) {
    $inverted_files{$new_schemas{$outdated}} = delete $inverted_files{$outdated};
  }
  %files = reverse %inverted_files;
}

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

# download fresh copies of all files, validate against their schemas
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, validate_formats => 1 });
  die $result->dump if not $result->valid;
}

# 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";
});

# only edit Changes file if there are updated files to be committed
if (system('git diff --quiet --stat share/') >> 8) {
  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';
}
else {
  say '# no changes to bundled schemas';
}
