<?php

declare(strict_types=1);

namespace Autogedal\FeedMonitor\Model\Adapter;

use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\FeedMonitor;
use Autogedal\FeedMonitor\Model\FeedRun;
use Autogedal\FeedMonitor\Model\Fetch\FeedFormatInterface;
use Autogedal\FeedMonitor\Model\Fetch\FeedReader;
use Autogedal\FeedMonitor\Model\Fetch\FeedResult;
use Autogedal\FeedMonitor\Model\JobRegistry;
use Autogedal\FeedMonitor\Model\Problem;
use Psr\Log\LoggerInterface;

/**
 * What a job reports: the feed it read (through the common reader), whether the update may start, how it ended.
 */
abstract class AbstractJobMonitor implements SourceMonitorInterface
{
    public function __construct(
        protected readonly FeedMonitor $monitor,
        protected readonly FeedReader $reader,
        protected readonly JobRegistry $jobs,
        protected readonly Config $config
    ) {
    }

    /**
     * Reads and validates a feed. Null when it cannot be used for the update (unreadable, invalid, empty or much
     * smaller than usual): the run is then already ended as failed and reported.
     *
     * @param array<int, mixed> $curlOptions extra CURLOPT_* values (e.g. a user agent)
     * @param LoggerInterface|null $logger the job's own log, which also gets the outcome
     */
    public function readFeed(
        FeedRun $run,
        string $url,
        FeedFormatInterface $format,
        array $curlOptions = [],
        ?LoggerInterface $logger = null
    ): ?FeedResult {
        $result = $this->reader->read($url, $format, $curlOptions);
        if (!$result->isOk()) {
            $logger?->info(sprintf('Feed not usable (%s): %s', $result->getProblem(), $result->getMessage()));
            $this->monitor->fail($run, (string)$result->getProblem(), $result->getMessage());

            return null;
        }
        $run->setRejectedRows($result->getSkipped());
        $logger?->info(sprintf(
            'Feed read: %d products%s.',
            $result->getCount(),
            $result->getSkipped() ? ', ' . $result->getSkipped() . ' rows not in the expected format rejected' : ''
        ));
        if (!$this->acceptItemCount($run, $result->getCount())) {
            $logger?->info('Feed has ' . $result->getCount() . ' products, none or far fewer than usual. Update skipped.');

            return null;
        }

        return $result;
    }

    /**
     * For data that is not an HTTP feed (an API answer, a saved list): false when there is nothing or far fewer
     * items than usual, the run is then already ended as failed and reported.
     */
    public function acceptItemCount(FeedRun $run, int $itemsRead): bool
    {
        return $this->monitor->acceptItemCount($run, $itemsRead);
    }

    public function success(FeedRun $run, int $itemsRead, int $itemsUpdated, string $message = ''): void
    {
        $this->monitor->success($run, $itemsRead, $itemsUpdated, $message);
    }

    /**
     * Ends the run with the result of the update: success, or, when some products could not be saved, a failed run
     * (problem "update", alert) with how many were saved and the first errors.
     *
     * Rows the feed format rejected and the job's own notes (e.g. prices ignored) are part of the result.
     *
     * @param array<int|string, string> $failures product code => error
     * @param string[] $notes what was left out on purpose, e.g. "3 preturi sub 1 leu ignorate"
     */
    public function complete(FeedRun $run, int $itemsRead, int $itemsUpdated, array $failures = [], array $notes = []): void
    {
        if ($run->getRejectedRows() > 0) {
            array_unshift($notes, $run->getRejectedRows() . ' randuri respinse (format invalid, neactualizate)');
        }
        $suffix = $notes ? ' ' . implode('; ', $notes) . '.' : '';
        if (!$failures) {
            $this->monitor->success($run, $itemsRead, $itemsUpdated, sprintf(
                '%d produse citite, %d actualizate.%s',
                $itemsRead,
                $itemsUpdated,
                $suffix
            ));
            return;
        }
        $first = array_slice($failures, 0, 5, true);
        $this->monitor->fail($run, Problem::UPDATE, sprintf(
            '%d produse nu au putut fi salvate, %d salvate. %s: %s.%s',
            count($failures),
            $itemsUpdated,
            count($failures) > count($first) ? 'Primele' : 'Produse',
            implode('; ', array_map(
                // numeric product codes (e.g. 13119) become integer keys
                static fn (int|string $code, string $error): string => $code . ' - ' . $error,
                array_keys($first),
                $first
            )),
            $suffix
        ), $itemsRead, $itemsUpdated);
    }

    /**
     * Failed rows returned by BasePriceStorage / SpecialPriceStorage::update(), as product code => error.
     *
     * @param \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] $results
     * @return array<string, string>
     */
    public static function priceUpdateFailures(array $results, string $label = ''): array
    {
        $failures = [];
        foreach ($results as $index => $result) {
            $parameters = (array)$result->getParameters();
            $replace = [];
            foreach ($parameters as $name => $value) {
                $replace['%' . $name] = is_scalar($value) ? (string)$value : '';
            }
            $code = (string)($parameters['SKU'] ?? $parameters['sku'] ?? ('rand ' . ($index + 1)));
            $failures[$code . ($label !== '' ? ' (' . $label . ')' : '')] = strtr((string)$result->getMessage(), $replace);
        }

        return $failures;
    }

    public function fail(FeedRun $run, string $problem, string $message, ?int $itemsRead = null): void
    {
        $this->monitor->fail($run, $problem, $message, $itemsRead);
    }

    public function failException(FeedRun $run, \Throwable $e): void
    {
        $this->monitor->failException($run, $e);
    }

    protected function getMaxAgeMinutes(string $jobCode): ?int
    {
        return $this->config->getMaxAgeMinutes($jobCode, $this->jobs->getDefaultMaxAgeMinutes($jobCode));
    }
}
