<?php

declare(strict_types=1);

namespace Autogedal\FeedMonitor\Model\Adapter;

use Autogedal\FeedMonitor\Model\Clock;
use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\FeedMonitor;
use Autogedal\FeedMonitor\Model\Fetch\RejectedRows;
use Autogedal\FeedMonitor\Model\Firebear\FirebearJobs;
use Autogedal\FeedMonitor\Model\Firebear\ImportHistory;
use Autogedal\FeedMonitor\Model\Firebear\ImportLog;
use Autogedal\FeedMonitor\Model\JobRegistry;
use Autogedal\FeedMonitor\Model\Problem;

/**
 * Firebear import jobs (active, scheduled by Firebear):
 * - the real result of each import (plugin on Firebear's SummaryHandler, matched to the run by its file), with the
 *   counts and errors of its log;
 * - imports started and never finished (Firebear history without an end: fatal error, killed process);
 * - jobs that have not started within the interval of their cron expression.
 * The feed is not downloaded separately: what counts is what Firebear's import did.
 */
class FirebearJobMonitor implements SourceMonitorInterface
{
    /** An import left unfinished longer ago than this is no longer "stuck" but "not running". */
    private const STUCK_WINDOW_HOURS = 48;

    public function __construct(
        private readonly FeedMonitor $monitor,
        private readonly JobRegistry $jobs,
        private readonly Config $config,
        private readonly ImportHistory $history,
        private readonly ImportLog $importLog,
        private readonly Clock $clock
    ) {
    }

    /**
     * Records the import that just ended, identified by its own file (history row + log), from the plugin on
     * Firebear's SummaryHandler. Jobs that are not active and scheduled are not monitored.
     *
     * Firebear has no guard of its own, so its result is judged after the import, with the rules of the custom feeds:
     * failed import, no row checked (empty feed), too many invalid rows, far fewer rows than the last successful run,
     * or a log without the rows checked (result that cannot be verified) are problems, not successes.
     */
    public function recordImport(int $jobId, string $file, bool $success): void
    {
        $jobCode = FirebearJobs::jobCode($jobId);
        if (!$this->jobs->has($jobCode)) {
            return;
        }

        $row = $this->history->getByFile($jobId, $file);
        $log = $this->importLog->read($file);
        $finished = $row['finished'] ?? $this->clock->now();
        $started = $row['started'] ?? $finished;
        // nothing new to add / change: Firebear writes no "Imported / Updated" line
        $updated = $log['checked'] !== null ? (int)$log['imported'] + (int)$log['updated'] : null;
        [$problem, $message] = $this->evaluate($jobCode, $success, $log);

        $this->monitor->recordCompleted(
            $jobCode,
            $problem,
            $message . ' Log: var/log/firebear/' . $file . '.log',
            $started,
            $finished,
            $log['checked'],
            $updated
        );
    }

    /**
     * @param array{found: bool, checked: int|null, invalid: int|null, imported: int|null, updated: int|null, errors: string[]} $log
     * @return array{0: string|null, 1: string} problem (null: success), message
     */
    private function evaluate(string $jobCode, bool $success, array $log): array
    {
        $errors = $log['errors'] ? ' Ultimele erori: ' . implode(' | ', $log['errors']) . '.' : '';
        if (!$success) {
            return [Problem::IMPORT, 'Firebear a raportat importul ca esuat.' . ($errors ?: ' Logul nu contine erori.')];
        }
        if (!$log['found'] || $log['checked'] === null) {
            return [Problem::INVALID, 'Rezultatul importului nu poate fi verificat: logul lipseste sau nu contine numarul de randuri verificate.'];
        }
        $checked = (int)$log['checked'];
        if ($checked === 0) {
            return [Problem::EMPTY, 'Importul nu a verificat niciun rand: feed-ul este gol.'];
        }
        $invalid = (int)$log['invalid'];
        if ($invalid / $checked > RejectedRows::DEFAULT_MAX_SHARE) {
            return [Problem::INVALID, sprintf(
                '%d din %d randuri (%d%%) sunt invalide, peste limita de %d%%.%s',
                $invalid,
                $checked,
                (int)round($invalid / $checked * 100),
                (int)round(RejectedRows::DEFAULT_MAX_SHARE * 100),
                $errors
            )];
        }
        try {
            $reason = $this->monitor->incompleteReason($jobCode, '', $checked);
        } catch (\Throwable $e) {
            return [Problem::EXCEPTION, 'Verificarea completitudinii importului nu a putut fi facuta: ' . $e->getMessage()];
        }
        if ($reason !== null && !$this->monitor->consumeAllowance($jobCode)) {
            return [Problem::INCOMPLETE, $reason . ' Importul Firebear a fost aplicat.'];
        }

        return [null, sprintf(
            'Import reusit: %d randuri verificate, %d importate / actualizate%s.',
            $checked,
            (int)$log['imported'] + (int)$log['updated'],
            $invalid ? ', ' . $invalid . ' randuri invalide ignorate' : ''
        )];
    }

    public function check(bool $outsideCron): array
    {
        $found = [];
        foreach ($this->jobs->getJobCodes(FirebearJobs::SOURCE) as $jobCode) {
            $jobId = (int)FirebearJobs::jobId($jobCode);
            $row = $this->history->getLatest($jobId);

            if ($row && $row['finished'] === null) {
                $age = $this->clock->now() - $row['started'];
                $stuckAfter = $this->config->getStuckAfterMinutes($jobCode);
                if ($age < $stuckAfter * 60) {
                    continue; // still running
                }
                if ($age <= self::STUCK_WINDOW_HOURS * 3600) {
                    $message = sprintf(
                        'Importul pornit la %s (UTC) nu s-a terminat nici dupa %s (istoricul Firebear nu are ora de final). Log: var/log/firebear/%s.log',
                        $this->clock->format($row['started']),
                        $this->monitor->duration($age),
                        $row['file']
                    );
                    $this->monitor->raise($jobCode, Problem::STUCK, $message);
                    $found[] = $jobCode . ': ' . $message;
                    continue;
                }
            }

            $maxAge = $this->config->getMaxAgeMinutes($jobCode, $this->jobs->getDefaultMaxAgeMinutes($jobCode));
            if ($maxAge === null) {
                continue; // no regular schedule
            }
            $line = $this->monitor->checkNotStartedSince(
                $jobCode,
                $row['started'] ?? null,
                $row !== null && $row['finished'] !== null,
                $maxAge
            );
            if ($line !== null) {
                $found[] = $line;
            }
        }

        return $found;
    }
}
