#!/usr/bin/perl

use v5.26;
use warnings;
use experimental 'signatures';

use utf8;

use Object::Pad 0.66;

use Future::AsyncAwait;
use Future::IO::Impl::IOAsync;

use Net::Prometheus;
use Metrics::Any::Adapter "Prometheus";
use Metrics::Any '$metrics';
our $metrics;

eval { require Future::AsyncAwait::Metrics };

$metrics->make_counter( chip_failures =>
   name        => "app_device_chip_sensor_chip_failures",
   description => "Number of times a failure has been reported, per chip",
   labels      => [qw( chip )],
);

STDOUT->autoflush(1);
STDOUT->binmode( ":encoding(UTF-8)" );

my $PORT;
my $VERBOSE;

my $STRFTIME = "%Y-%m-%d %H:%M:%S";

class MyApp :isa(App::Device::Chip::sensor)
{
   use Carp;
   use POSIX qw( strftime );
   use Scalar::Util qw( refaddr reftype );

   method OPTSPEC
   {
      return $self->SUPER::OPTSPEC, (
         'port=i'     => \$PORT,
         'verbose+'   => \$VERBOSE,
         'config|c=s' => sub { $self->read_config_file( $_[1] ); },
      );
   }

   method read_config_file ( $path )
   {
      require YAML;

      # Config file is YAML and can override a number of settings, and add chips
      my $config = YAML::LoadFile( $path );

      if( $config->{adapters} ) {
         foreach my $c ( ( delete $config->{adapters} )->@* ) {
            my $type = delete $c->{type} // croak "Expected adapter config to have a 'type'";
            my $chips = delete $c->{chips} // croak "Expected adapter config to have a 'chips'";

            # TODO: It'd be really nice if Device::Chip::Adapter->make( $type, %opts ) existed
            my $desc = $type . join( ":", "", map { "$_=$c->{$_}" } sort keys %$c );
            my $adapter = Device::Chip::Adapter->new_from_description( $desc );

            foreach my $c ( $chips->@* ) {
               my ( $type, %config ) = ( reftype($c)//"" eq "HASH" )
                  ? ( delete $c->{type}, $c->%* )
                  : ( $c, () );

               $self->add_chip(
                  type      => $type,
                  adapter   => $adapter,
                  mountopts => $config{mountopts},
                  config    => $config{config},
               );
            }
         }
      }

      if( exists $config->{port} ) {
         $PORT = $config->{port};
         delete $config->{port};
      }

      foreach my $opt (qw( interval best_effort filter )) {
         exists $config->{$opt} and
            $self->$opt = delete $config->{$opt};
      }

      # Legacy support
      exists $config->{mid3} and
         $self->filter = "mid3", delete $config->{mid3};

      die "Unrecognised config file items " . join( ", ", sort keys %$config ) . "\n"
         if keys %$config;
   }

   async method after_sensors :override ( @sensors )
   {
      my %got;

      foreach my $sensor ( @sensors ) {
         my $name = $sensor->name;
         my $units = $sensor->units;

         my $chip = $sensor->chip;
         my $chipname = ( ref $chip ) =~ s/^Device::Chip:://r;

         # Convert some common characters that Prometheus will get upset by
         if( defined $units ) {
            $units =~ s{/}{_per_};

            $units =~ s/%/percentage/g;
            $units =~ s/°/degrees/g;
            $units =~ s/µ/micro/g;

            $units =~ s/m²/square_metre/g;
            $units =~ s/m³/cubic_metre/g;
         }

         $got{$name}++ and next;

         my $m = "make_" . $sensor->type;
         $metrics->$m( $name,
            name        => join( "_", "sensor", $name, grep { defined } $units ),
            description => "Sensor $name",
            labels      => [qw( chip )],
         );

         $metrics->inc_counter_by( chip_failures => 0, [ chip => $chipname ] );
      }
   }

   # Count the failures per reading round
   field %failures_per_chip;
   # Counts in total
   field %chip_failures;

   method output_readings ( $now, $sensors, $values )
   {
      if( $VERBOSE ) {
         printf "At %s\n", strftime( $STRFTIME, localtime );
         $self->print_readings( $sensors, $values );
      }

      foreach my $i ( 0 .. $#$sensors ) {
         my $sensor = $sensors->[$i];
         my $value  = $values->[$i];

         my $chip = $sensor->chip;
         my $chipname = ( ref $chip ) =~ s/^Device::Chip:://r;

         my $m = ( $sensor->type eq "gauge" )   ? "set_gauge_to" :
                 ( $sensor->type eq "counter" ) ? "inc_counter_by" :
                                                  die "Unrecognised sensor type";
         $metrics->$m( $sensor->name,
            $self->_format_reading( $sensor, $value ), [ chip => $chipname ] );
      }

      foreach my $chipaddr ( keys %failures_per_chip ) {
         if( ++$chip_failures{ $chipaddr } >= 5 ) {
            die "This sensor chip has failed 5 times in a row; aborting\n";
         }
         if( keys %chip_failures >= 3 ) {
            die "Three or more sensor chips have failed; aborting\n";
         }
      }
      undef %failures_per_chip;
   }

   method on_sensor_fail ( $sensor, $failure ) {
      $self->SUPER::on_sensor_fail( $sensor, $failure );

      my $chip = $sensor->chip;
      my $chipname = ( ref $chip ) =~ s/^Device::Chip:://r;

      $metrics->inc_counter( chip_failures => [ chip => $chipname ] );

      $failures_per_chip{ refaddr $chip } = 1;
   }

   method on_sensor_ok ( $sensor ) {
      delete $chip_failures{ refaddr $sensor->chip };
   }
}

my $client = Net::Prometheus->new;

my $app = MyApp->new->parse_argv;

$client->export_to_IO_Async( undef, port => $PORT );

await $app->run;
