#!/usr/bin/env perl
# -*- perl -*-

#
# Author: Slaven Rezic
#
# Copyright (C) 2021,2022,2023,2025,2026 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# WWW:  https://github.com/eserte/bbbike/
#

use strict;
use warnings;

use FindBin;
use lib "$FindBin::RealBin/..";

use File::Copy qw(cp);
use File::ReadBackwards;
use File::Temp;
use JSON::XS qw(decode_json);
use Getopt::Long;
use LWP::UserAgent;

use BBBikeYAML qw(LoadFile);

# list of failing ids causing errors like
#   {"error":{"message":"Unsupported get request","type":"MLYApiException","code":100,"error_subcode":33,"fbtrace_id":"..."}}
my %skip_ids = map {($_,1)} qw(2206962903434296 2040453403524566 1536099294668850);

my $token;
my $with_make = 1;
GetOptions(
	   "t|token=s" => \$token,
	   'with-make!' => \$with_make,
	   'without-make' => sub { $with_make = 0 },
	  )
    or die "usage: $0 [--token token] [--with-make|--without-make] file.bbd ...\n";
my @filenames = @ARGV
    or die "Please provide one or more filenames";

if (!$token) {
    my $conf_file = "$ENV{HOME}/.mapillary";
    my $conf = eval { LoadFile $conf_file }
	or die "Please provide --token or create a YAML file '$conf_file' with a 'client_token' entry for the Mapillary API token\n";
    $token = $conf->{client_token} || die "Can't get client_token from $conf_file";
}

my $ua = LWP::UserAgent->new;
my $url_short = 'https://graph.mapillary.com';
my $url = "$url_short/graphql";
$ua->default_header(Authorization => "OAuth $token");

my $image_id_to_make = $with_make ? create_image_id_to_make_cache(@filenames) : {};

for my $filename (@filenames) {
    my $has_changes;
    my $fh = File::ReadBackwards->new($filename)
	or die "Can't open $filename: $!";
    my $current_creator;
    my @new_lines;
    while(defined($_ = $fh->readline)) {
	if (/^#/) {
	    if ($current_creator) {
		if      (m{^\Q#: url: https://www.mapillary.com/app/user/\E\d+}) {
		    s{(\Q#: url: https://www.mapillary.com/app/user/\E)\d+}{$1 . $current_creator}e;
		    $has_changes++;
		} elsif (m{^\Q#: url: https://www.mapillary.com/app/?}) {
		    s{^(\Q#: url: https://www.mapillary.com/app/\E)}{$1 . "user/$current_creator"}e;
		    $has_changes++;
		}
	    }
	    unshift @new_lines, $_;
	} else {
	    chomp;
	    if ($with_make && /\bmake=($|\s)/) { # empty make
		if (/\bstart_id=(\d+)/) {
		    my $image_id = $1;
		    if (!$skip_ids{$image_id}) {
			my $make;
			if (defined $image_id_to_make->{$image_id}) {
			    $make = $image_id_to_make->{$image_id};
			} else {
			    my $data = cached_graph_api_call("$image_id?fields=id,make");
			    $make = $data->{make};
			}
			if (!$make) {
			    warn "WARNING: cannot find make for image id $image_id, skipping...\n";
			} else {
			    s/\bmake=/make=$make/;
			    warn "INFO: inject make $make.\n";
			    $has_changes++;
			}
		    } else {
			warn "WARNING: image id '$image_id' in skip list, skipping...\n";
		    }
		} else {
		    warn "WARNING: empty make, but no image id (start_id) found in line, skipping...\n";
		}
	    }
	    if (/\bcreator=(\d+)/) { # numerical id, needs to be translated
		my $creator_id = $1;
		my $data = cached_graph_api_call("$creator_id?fields=username,id");
		my $creator_username = $data->{username}
		    or die "No username found in response for $creator_id\n";
		$current_creator = $creator_username;
		s/\bcreator=\d+/creator=$creator_username/;
		warn "INFO: changed creator userid $creator_id to username $creator_username.\n";
		$has_changes++;
	    } elsif (/\bcreator=(\S+)/) {
		# creator already set, skip silently
		$current_creator = $1;
	    } else {
		if (/\bstart_id=(\d+)/) {
		    my $start_id = $1;
		    my $data = graph_api_call("$start_id?fields=creator");
		    if (!$data) {
			die "No data returned for $start_id";
		    }
		    my $creator = $data->{creator}->{username};
		    if (!$creator) {
			die "Cannot get creator.username for $start_id";
		    }
		    $current_creator = $creator;
		    if (!s/(\bstart_captured_at=\S+)/$1 creator=$creator/) {
			die "Susbstitution in '$_' (adding creator=$creator) failed";
		    }
		    warn "INFO: added creator $creator for sequence with image $start_id.\n";
		    $has_changes++;
		} else {
		    warn "WARNING: Cannot parse start_id from $_, skipping";
		    undef $current_creator;
		}
	    }
	    unshift @new_lines, "$_\n";
	}
    }

    if ($has_changes) {
	my $ofh = File::Temp->new;
	print $ofh join '', @new_lines;
	$ofh->close
	    or die "Problem while writing to temporary file: $!";
	# sanity check: number of lines should not change
	my $old_line_number = `wc -l $filename`; ($old_line_number) = $old_line_number =~ m{^\s*(\d+)};
	my $new_line_number = `wc -l $ofh`; ($new_line_number) = $new_line_number =~ m{^\s*(\d+)};
	if ($old_line_number != $new_line_number) {
	    die "ERROR: changed file does not have same line count as old file ($new_line_number != $old_line_number)";
	}
	cp "$ofh", "$filename~"
	    or die "Can't copy to $filename~: $!";
	copy_stat($filename, "$filename~");
	rename "$filename~", $filename
	    or die "Can't rename $filename~ to $filename: $!";
	warn "INFO: $filename was changed\n";
    } else {
	warn "INFO: $filename unchanged\n";
    }
}

{
    my %cache;
    sub cached_graph_api_call {
	my($query) = @_;
	return $cache{$query} if exists $cache{$query};
	my $data = graph_api_call($query);
	$cache{$query} = $data;
	$data;
    }
}

sub graph_api_call {
    my($query) = @_;
    my $resp;
 TRY: {
	my $max_tries = 2;
	for my $try (1..$max_tries) {
	    my $api_url = "$url_short/$query";
	    warn "DEBUG: fetch $api_url...\n";
	    $resp = $ua->get($api_url);
	    last TRY if $resp->is_success;
	    warn "Request $try/$max_tries failed: " . $resp->dump;
	    sleep 1 if $try < $max_tries;
	}
	die "Maximum number of tries ($max_tries) reached, failing permanently\n";
    }
    my $json = $resp->decoded_content;
    my $data = decode_json($json);
    $data;
}

# Performance: do batch requests to create a image_id->make mapping
sub create_image_id_to_make_cache {
    my(@filenames) = @_;
    my %start_ids;
    for my $filename (@filenames) {
	open my $fh, '<', $filename
	    or die "Can't open $filename: $!";
	while(<$fh>) {
	    next if /^#/;
	    if (/\bmake=($|\s)/) { # empty make
		if (/\bstart_id=(\d+)/) {
		    $start_ids{$1} = 1;
		}
	    }	    
	}
    }
    my %image_id_to_make;
    if (%start_ids) {
	my @start_ids = grep { !$skip_ids{$_} } sort { $a<=>$b } keys %start_ids;
	while(@start_ids) {
	    my @batch = splice @start_ids, 0, 50; # possibly number of max. elements per batch call
	    my $data = graph_api_call('?fields=make&ids='.join(',',@batch));
	    while(my($start_id,$v) = each %$data) {
		$image_id_to_make{$start_id} = $v->{make};
	    }
	}
    }
    \%image_id_to_make;
}

# REPO BEGIN
# REPO NAME copy_stat /home/e/eserte/src/srezic-repository 
# REPO MD5 f567def1f7ce8f3361e474b026594660

sub copy_stat {
    my($src, $dest) = @_;
    my @stat = ref $src eq 'ARRAY' ? @$src : stat($src);
    die "Can't stat $src: $!" if !@stat;

    chmod $stat[2], $dest
	or warn "Can't chmod $dest to " . sprintf("0%o", $stat[2]) . ": $!";
    chown $stat[4], $stat[5], $dest
	or do {
	    my $save_err = $!; # otherwise it's lost in the get... calls
	    warn "Can't chown $dest to " .
		 (getpwuid($stat[4]))[0] . "/" .
                 (getgrgid($stat[5]))[0] . ": $save_err";
	};
    utime $stat[8], $stat[9], $dest
	or warn "Can't utime $dest to " .
	        scalar(localtime $stat[8]) . "/" .
		scalar(localtime $stat[9]) .
		": $!";
}
# REPO END

__END__

=head1 NAME

mapillary-inject-metadata - add metadata (creator and make) to mapillary-originated bbd files

=head1 SYNOPSIS

    mapillary-inject-metadata [--token ...] [--with-make|--without-make] file.bbd

=head1 DESCRIPTION

When creating bbd files from Mapillary vector tiles using
L<mapillary-mvt-fetch>, then sequence information like creator or make
are missing. This script injects the missing information.

Typical workflow is

    mapillary-mvt-fetch YYYYMMDD >| YYYYMMDD.bbd
    mapillary-inject-metadata YYYYMMDD.bbd

This script requires a Mapillary API token, either supplied using the
C<--token> option, or stored in a YAML file F<~/.mapillary> as hash
entry C<client_token>:

    client_token: MLY|...

=head1 AUTHOR

Slaven Rezic

=head1 SEE ALSO

L<mapillary-mvt-fetch>.

=cut
