<?php

namespace AIPT\Api\AutomationJobs;

use AIPT\Api\AutomationJobs\Interfaces\JobLoggerInterface;
use AIPT\Api\AutomationJobs\Models\JobLog;

/**
 * Job Logger
 *
 * Handles logging for automation jobs
 * Following Single Responsibility Principle
 *
 * @since 2.3.0
 */
class JobLogger implements JobLoggerInterface {

    /**
     * Maximum byte length for the persisted message column.
     *
     * The schema uses TEXT (≈64 KB). Provider error strings and verbose
     * `sprintf` outputs can run into the tens of KB; capping at 4 KB keeps
     * one noisy log entry from dominating the row size while still leaving
     * enough room for any realistic single-line error.
     */
    public const MAX_MESSAGE_BYTES = 4096;

    /**
     * Maximum byte length for the persisted context JSON column.
     *
     * The schema uses LONGTEXT (≈4 GB) which is dangerous: a single
     * `raw_results_sample` or `all_settings` dump can be tens of KB.
     * 16 KB lets us keep enough JSON to triage with while bounding the
     * single-row contribution to overall bloat.
     */
    public const MAX_CONTEXT_BYTES = 16384;

    /**
     * WordPress database instance
     * @var \wpdb
     */
    private $wpdb;

    /**
     * Job logs table name
     * @var string
     */
    private $logs_table;

    /**
     * Constructor
     */
    public function __construct() {
        global $wpdb;
        $this->wpdb = $wpdb;
        $this->logs_table = $wpdb->prefix . 'aipt_job_logs';
    }

    /**
     * Log an info message
     *
     * @param int $job_id Job ID
     * @param string $message Log message
     * @param array $context Additional context data
     * @return bool Success status
     */
    public function info($job_id, $message, $context = []) {
        return $this->log(JobLog::info($job_id, $message, $context));
    }

    /**
     * Log a warning message
     *
     * @param int $job_id Job ID
     * @param string $message Log message
     * @param array $context Additional context data
     * @return bool Success status
     */
    public function warning($job_id, $message, $context = []) {
        return $this->log(JobLog::warning($job_id, $message, $context));
    }

    /**
     * Log an error message
     *
     * @param int $job_id Job ID
     * @param string $message Log message
     * @param array $context Additional context data
     * @return bool Success status
     */
    public function error($job_id, $message, $context = []) {
        return $this->log(JobLog::error($job_id, $message, $context));
    }

    /**
     * Log a debug message
     *
     * @param int $job_id Job ID
     * @param string $message Log message
     * @param array $context Additional context data
     * @return bool Success status
     */
    public function debug($job_id, $message, $context = []) {
        return $this->log(JobLog::debug($job_id, $message, $context));
    }

    /**
     * Log a message
     *
     * @param JobLog $log Log entry
     * @return bool Success status
     */
    private function log(JobLog $log) {

        $validation_errors = $log->validate();
        if (!empty($validation_errors)) {
            return false;
        }



        $data = $log->toArray();
        unset($data['id']); 


        if (!empty($data['context'])) {
            $data['context'] = $this->redact_context_string($data['context']);
        }


        if (!empty($data['message'])) {
            $data['message'] = self::truncate((string) $data['message'], self::MAX_MESSAGE_BYTES);
        }
        if (!empty($data['context'])) {

            $data['context'] = self::truncateContextPreservingJson(
                (string) $data['context'],
                self::MAX_CONTEXT_BYTES
            );
        }


        $result = $this->wpdb->insert(
            $this->logs_table,
            $data,
            [
                '%d', 
                '%s', 
                '%s', 
                '%s', 
                '%s'  
            ]
        );

        return $result !== false;
    }

    /**
     * Get logs for a job
     *
     * @param int $job_id Job ID
     * @param array $filters Filters (level, type, limit, offset)
     * @return array Job logs
     */
    public function getJobLogs($job_id, $filters = []) {
        $where_conditions = ['job_id = %d'];
        $where_values = [$job_id];


        if (!empty($filters['level'])) {
            $where_conditions[] = 'level = %s';
            $where_values[] = $filters['level'];
        }


        if (!empty($filters['type'])) {
            $where_conditions[] = "context LIKE %s";

            $where_values[] = '%' . $this->wpdb->esc_like('"type":"' . sanitize_text_field((string)$filters['type']) . '"') . '%';
        }


        $query = "SELECT * FROM {$this->logs_table} WHERE " . implode(' AND ', $where_conditions);
        $query .= " ORDER BY created_at DESC";


        $limit = isset($filters['limit']) ? (int) $filters['limit'] : 50;
        $offset = isset($filters['offset']) ? (int) $filters['offset'] : 0;

        $query .= " LIMIT %d OFFSET %d";
        $where_values[] = $limit;
        $where_values[] = $offset;


        $results = $this->wpdb->get_results(
            $this->wpdb->prepare($query, $where_values),
            ARRAY_A
        );



        $logs = [];
        foreach ($results as $result) {
            $logs[] = new JobLog($result);
        }

        return $logs;
    }

    /**
     * Get logs by level
     *
     * @param int $job_id Job ID
     * @param string $level Log level
     * @param int $limit Maximum number of logs
     * @param int $offset Offset for pagination
     * @return array Job logs
     */
    public function getLogsByLevel($job_id, $level, $limit = 50, $offset = 0) {
        return $this->getJobLogs($job_id, [
            'level' => $level,
            'limit' => $limit,
            'offset' => $offset
        ]);
    }

    /**
     * Clear logs for a job
     *
     * @param int $job_id Job ID
     * @param string|null $level Optional level filter
     * @return bool Success status
     */
    public function clearJobLogs($job_id, $level = null) {
        $where = ['job_id' => $job_id];
        $where_format = ['%d'];

        if ($level !== null) {
            $where['level'] = $level;
            $where_format[] = '%s';
        }

        $result = $this->wpdb->delete(
            $this->logs_table,
            $where,
            $where_format
        );

        return $result !== false;
    }

    /**
     * Get log statistics for a job
     *
     * @param int $job_id Job ID
     * @return array Log statistics by level
     */
    public function getLogStatistics($job_id) {
        $query = "
            SELECT
                level,
                COUNT(*) as count
            FROM {$this->logs_table}
            WHERE job_id = %d
            GROUP BY level
        ";


        $results = $this->wpdb->get_results(
            $this->wpdb->prepare($query, $job_id),
            ARRAY_A
        );



        $statistics = [
            'info' => 0,
            'warning' => 0,
            'error' => 0,
            'debug' => 0,
            'total' => 0
        ];


        foreach ($results as $result) {
            $statistics[$result['level']] = (int) $result['count'];
            $statistics['total'] += (int) $result['count'];
        }

        return $statistics;
    }

    /**
     * Clean old logs
     *
     * @param int $days_to_keep Number of days to keep logs
     * @return int Number of logs deleted
     */
    public function cleanOldLogs($days_to_keep = 30) {
        $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$days_to_keep} days"));


        $result = $this->wpdb->query(
            $this->wpdb->prepare(
                "DELETE FROM {$this->logs_table} WHERE created_at < %s",
                $cutoff_date
            )
        );


        return $result !== false ? $result : 0;
    }

    /**
     * Get recent error logs across all jobs
     *
     * @param int $limit Maximum number of logs
     * @return array Recent error logs
     */
    public function getRecentErrors($limit = 20) {
        $query = "
            SELECT l.*, j.name as job_name
            FROM {$this->logs_table} l
            LEFT JOIN {$this->wpdb->prefix}aipt_automation_jobs j ON l.job_id = j.id
            WHERE l.level = 'error'
            ORDER BY l.created_at DESC
            LIMIT %d
        ";


        $results = $this->wpdb->get_results(
            $this->wpdb->prepare($query, $limit),
            ARRAY_A
        );


        $logs = [];
        foreach ($results as $result) {
            $log = new JobLog($result);
            $log->job_name = $result['job_name'] ?? 'Unknown Job';
            $logs[] = $log;
        }

        return $logs;
    }

    /**
     * Get log count for a job
     *
     * @param int $job_id Job ID
     * @param string|null $level Optional level filter
     * @return int Log count
     */
    public function getLogCount($job_id, $level = null) {
        $where_conditions = ['job_id = %d'];
        $where_values = [$job_id];

        if ($level !== null) {
            $where_conditions[] = 'level = %s';
            $where_values[] = $level;
        }

        $query = "SELECT COUNT(*) FROM {$this->logs_table} WHERE " . implode(' AND ', $where_conditions);


        return (int) $this->wpdb->get_var(
            $this->wpdb->prepare($query, $where_values)
        );

    }

    /**
     * Get recent logs across all jobs
     *
     * @param int $limit Number of logs to retrieve
     * @return array Recent log entries
     */
    public function getRecentLogs($limit = 50) {
        $query = "
            SELECT
                job_id,
                level,
                message,
                context,
                created_at
            FROM {$this->logs_table}
            ORDER BY created_at DESC
            LIMIT %d
        ";


        $results = $this->wpdb->get_results(
            $this->wpdb->prepare($query, $limit)
        );


        return $results ?: [];
    }

    /**
     * Truncate a string to at most $max_bytes, appending a visible marker
     * so a human reading the log can tell something was cut.
     *
     * Public + static so JobLogCleaner / monitoring helpers can apply the
     * same caps when ingesting external strings (e.g. on a one-time
     * pre-bloat sweep) and so the cap can be unit-tested without standing
     * up a wpdb mock.
     */
    /**
     * Truncate a context payload while keeping it JSON-decodable.
     *
     * When the input was JSON-shaped (the standard path — redact_context_string()
     * runs wp_json_encode() on the context before insert), produces a valid
     * envelope `{"truncated":true,"raw_excerpt":"…"}` whose raw_excerpt holds
     * the byte-capped head of the original string. When the input was not
     * JSON (legacy callers piping a plain string into JobLogger::log()), falls
     * back to the plain byte truncation.
     *
     * Public + static for the same reasons truncate() is public: lets cron
     * cleaners / one-time pre-bloat sweeps apply the same shape.
     */
    public static function truncateContextPreservingJson(string $value, int $max_bytes): string {
        if (strlen($value) <= $max_bytes) {
            return $value;
        }

        $decoded  = json_decode($value, true);
        $was_json = json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded));

        if (!$was_json) {
            return self::truncate($value, $max_bytes);
        }


        $envelope_overhead = 96;
        $excerpt_budget    = max(0, $max_bytes - $envelope_overhead);

        $encoded = wp_json_encode(
            [
                'truncated'   => true,
                'raw_excerpt' => self::truncate($value, $excerpt_budget),
            ]
        );

        if ($encoded === false) {
            return self::truncate($value, $max_bytes);
        }


        if (strlen($encoded) > $max_bytes) {
            $shrunk_budget = max(0, intdiv($excerpt_budget, 2));
            $retry         = wp_json_encode(
                [
                    'truncated'   => true,
                    'raw_excerpt' => self::truncate($value, $shrunk_budget),
                ]
            );
            if (is_string($retry) && strlen($retry) <= $max_bytes) {
                return $retry;
            }
            return self::truncate($value, $max_bytes);
        }

        return $encoded;
    }

    public static function truncate(string $value, int $max_bytes): string {
        if (strlen($value) <= $max_bytes) {
            return $value;
        }
        $marker  = '… (truncated)';
        $keep    = max(0, $max_bytes - strlen($marker));

        if (function_exists('mb_strcut')) {
            return mb_strcut($value, 0, $keep, 'UTF-8') . $marker;
        }

        return substr($value, 0, $keep) . $marker;
    }

    /**
     * Redact sensitive information from JSON context string
     */
    private function redact_context_string($json) {
        $arr = json_decode($json, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($arr)) {
            return $json;
        }
        $redacted = $this->redact_array_recursive($arr);
        return wp_json_encode($redacted);
    }

    /**
     * Recursively redact sensitive keys in array
     */
    private function redact_array_recursive($data) {
        if (!is_array($data)) { return $data; }
        $sensitive_keys = ['api_key','apikey','authorization','token','secret','password','bearer','openai_api_key','openrouter_api_key','gemini_api_key'];
        foreach ($data as $k => $v) {
            if (is_array($v)) {
                $data[$k] = $this->redact_array_recursive($v);
                continue;
            }
            $lk = strtolower((string)$k);
            foreach ($sensitive_keys as $needle) {
                if (strpos($lk, $needle) !== false) {
                    $val = (string) $v;
                    if ($val === '') { $data[$k] = ''; break; }

                    $data[$k] = str_repeat('*', max(0, strlen($val) - 4)) . substr($val, -4);
                    break;
                }
            }
        }
        return $data;
    }

}
