<?php

namespace auctionforge\inc\sync;

use auctionforge\inc\InstanceDB;

class SyncLogger
{
    const TABLE_NAME = 'af_sync_log';

    /**
     * Get the full table name with prefix
     * @return string
     */
    public static function getTableName()
    {
        $db = InstanceDB::getInstanceDB();
        return $db->prefix . self::TABLE_NAME;
    }

    /**
     * Write a log entry
     *
     * @param string      $context     e.g. 'webhook', 'worker', 'queue', 'safety', 'system', 'admin'
     * @param string      $message     Human-readable log message
     * @param string      $level       'info', 'success', 'warning', 'error'
     * @param string|null $auctionId   BidSpirit auction ID (optional)
     * @param int|null    $jobId       Queue job ID (optional)
     * @return int|false  The log entry ID, or false on failure
     */
    public static function log($context, $message, $level = 'info', $auctionId = null, $jobId = null)
    {
        $db = InstanceDB::getInstanceDB();
        $table = self::getTableName();

        // Safety: check table exists (avoid errors during initial setup)
        if ($db->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
            return false;
        }

        $result = $db->insert($table, [
            'context'    => $context,
            'message'    => mb_substr($message, 0, 1000),
            'level'      => $level,
            'auction_id' => $auctionId,
            'job_id'     => $jobId,
            'created_at' => current_time('mysql', true),
        ], ['%s', '%s', '%s', '%s', '%d', '%s']);

        if ($result === false) {
            return false;
        }

        $insertId = (int) $db->insert_id;

        // Fan out to wpdeploy / Telegram on errors. Listener uses non-blocking HTTP
        // so the logger never blocks even when the relay is unreachable.
        // Some "errors" are transient retry conditions that don't warrant alerting
        // (e.g. BidSpirit API returns 0 lots while the catalog is being finalised
        // — the safety cron will retry the job automatically). Filter those out
        // before firing the fan-out action.
        if ($level === 'error' && function_exists('do_action')
            && !self::isSuppressedFromAlerts($message, $context)) {
            do_action('auctionforge_sync_error_logged', $insertId, $context, $message, $auctionId, $jobId);
        }

        return $insertId;
    }

    /**
     * Returns true when an error log message is known to be transient/benign
     * and should not be relayed to wpdeploy / Telegram. The local DB row is
     * still written so the in-app sync console keeps showing retries.
     *
     * Extendable by other code via the `auctionforge_sync_alert_suppress_patterns`
     * filter — return additional substring patterns to suppress.
     */
    private static function isSuppressedFromAlerts($message, $context)
    {
        $defaults = array(
            // Catalog hasn't been published yet — webhook fires before BidSpirit
            // has finalised the catalog. The safety cron retries automatically.
            'BidSpirit API returned 0 lots',
            'catalog may not be ready yet',
        );
        $patterns = function_exists('apply_filters')
            ? apply_filters('auctionforge_sync_alert_suppress_patterns', $defaults, $context, $message)
            : $defaults;

        foreach ((array) $patterns as $needle) {
            if ($needle !== '' && stripos($message, $needle) !== false) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get recent log entries (for admin live console polling)
     *
     * @param int    $sinceId   Return entries with id > $sinceId (for incremental polling)
     * @param int    $limit     Max entries to return
     * @param string $minLevel  Minimum level filter: 'info', 'warning', 'error'
     * @return array
     */
    public static function getRecent($sinceId = 0, $limit = 50, $minLevel = 'info')
    {
        $db = InstanceDB::getInstanceDB();
        $table = self::getTableName();

        $levels = self::getLevelsAbove($minLevel);
        $placeholders = implode(',', array_fill(0, count($levels), '%s'));

        $args = array_merge([$sinceId], $levels, [$limit]);

        $sql = "SELECT * FROM `{$table}` WHERE id > %d AND level IN ({$placeholders}) ORDER BY id ASC LIMIT %d";

        return $db->get_results($db->prepare($sql, ...$args));
    }

    /**
     * Get the latest log ID (for initializing the polling cursor)
     *
     * @return int
     */
    public static function getLatestId()
    {
        $db = InstanceDB::getInstanceDB();
        $table = self::getTableName();

        $id = $db->get_var("SELECT MAX(id) FROM `{$table}`");
        return (int) $id;
    }

    /**
     * Get log entries for a specific job
     *
     * @param int $jobId
     * @return array
     */
    public static function getForJob($jobId)
    {
        $db = InstanceDB::getInstanceDB();
        $table = self::getTableName();

        return $db->get_results($db->prepare(
            "SELECT * FROM `{$table}` WHERE job_id = %d ORDER BY id ASC",
            $jobId
        ));
    }

    /**
     * Purge old log entries older than $days
     *
     * @param int $days
     * @return int  Number of rows deleted
     */
    public static function purge($days = 7)
    {
        $db = InstanceDB::getInstanceDB();
        $table = self::getTableName();
        $cutoff = gmdate('Y-m-d H:i:s', time() - ($days * 86400));

        $count = $db->query($db->prepare(
            "DELETE FROM `{$table}` WHERE created_at < %s",
            $cutoff
        ));

        return (int) $count;
    }

    /**
     * Get levels at or above the given level
     *
     * @param string $minLevel
     * @return array
     */
    private static function getLevelsAbove($minLevel)
    {
        $hierarchy = ['info', 'success', 'warning', 'error'];
        $index = array_search($minLevel, $hierarchy);
        if ($index === false) {
            return $hierarchy;
        }
        return array_slice($hierarchy, $index);
    }
}
