<?php

declare(strict_types=1);

namespace KimaAI;

use KimaAI\Admin\Settings;

defined('ABSPATH') || exit;

final class Logger {
  private const FILE_PREFIX = 'kimaai';
  private const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
  private const ROTATE_CHECK_EVERY = 25; // check every N writes
  private const TRACE_LIMIT = 12;

  private const LEVELS = [
    'debug' => 10,
    'info' => 20,
    'notice' => 30,
    'warn' => 40,
    'error' => 50,
  ];

  /**
   * Plugin name used in mirrored log messages.
   *
   * @var string
   */
  private static string $plugin_name = '';

  /**
   * Option name storing logger data.
   *
   * @var string
   */
  private static string $option_key = '';

  /**
   * Current log file path.
   *
   * @var string|null
   */
  private static ?string $log_file = null;

  /**
   * Whether log entries should be mirrored to WP-CLI.
   *
   * @var bool
   */
  private static bool $cli_output = false;

  /**
   * Number of writes since the last rotation check.
   *
   * @var int
   */
  private static int $counter = 0;

  /**
   * Minimum enabled log level.
   *
   * @var string
   */
  private static string $min_level = 'info';

  /**
   * Initializes the logger.
   *
   * @param string $option_key Option name storing logger data.
   * @param string $plugin_name Plugin name.
   * @param bool $cli_output Whether to mirror logs to WP-CLI.
   * @throws \Exception
   */
  public static function init(string $option_key, string $plugin_name, bool $cli_output = false): void {
    self::$option_key = $option_key;
    self::$plugin_name = $plugin_name;
    self::$cli_output = $cli_output || (defined('WP_CLI') && WP_CLI);

    add_action('rest_api_init', [__CLASS__, 'registerRestEndpoint']);

    self::$log_file = self::ensureLogFile();

    $level_value = apply_filters(
      'kimaai_log_level',
      KimaAI()->classes->get(Settings::class)->getOption('logLevel', 'tools', 'logSettings')
    );

    if (is_string($level_value)) {
      self::setMinLevel($level_value);
    }
  }

  /**
   * Sets the minimum enabled log level.
   *
   * @param string $level Log level.
   */
  public static function setMinLevel(string $level): void {
    $level = strtolower($level);
    self::$min_level = array_key_exists($level, self::LEVELS) ? $level : 'debug';
  }

  /**
   * Checks whether a level is below the configured threshold.
   *
   * @param string $level Log level.
   */
  private static function belowThreshold(string $level): bool {
    $level_key = strtolower($level);
    $level_weight = self::LEVELS[$level_key] ?? 999;
    $minimum_weight = self::LEVELS[self::$min_level] ?? 20;

    return $level_weight < $minimum_weight;
  }

  /**
   * Registers the log REST endpoint.
   */
  public static function registerRestEndpoint(): void {
    register_rest_route('kimaai/v1', '/show-log', [
      'methods' => 'GET',
      'callback' => fn() => rest_ensure_response(['content' => rtrim(self::getContents())]),
      'permission_callback' => fn() => current_user_can('manage_options'),
    ]);
  }

  /**
   * Writes a debug log entry.
   *
   * @param string $message Message to log.
   */
  public static function debug(string $message): void {
    if (WP_DEBUG) {
      if (stripos($message, 'embedding') !== false) {
        $message = substr($message, 0, 200) . (strlen($message) > 200 ? '...' : '');
      }

      self::write('DEBUG', $message);
    }
  }

  /**
   * Writes an info log entry.
   *
   * @param string $message Message to log.
   */
  public static function info(string $message): void {
    self::write('INFO', $message);
  }

  /**
   * Writes a notice log entry.
   *
   * @param string $message Message to log.
   */
  public static function notice(string $message): void {
    self::write('NOTICE', $message);
  }

  /**
   * Writes a warning log entry.
   *
   * @param string $message Message to log.
   */
  public static function warn(string $message): void {
    self::write('WARN', $message, true);
  }

  /**
   * Writes an error log entry.
   *
   * @param string $message Message to log.
   */
  public static function error(string $message): void {
    self::write('ERROR', $message, true);
  }

  /**
   * Writes a log entry at a dynamic level.
   *
   * @param string $message Message to log.
   * @param string $level Log level.
   */
  public static function log(string $message, string $level = 'info'): void {
    $level = strtolower($level);

    if (!method_exists(self::class, $level)) {
      $level = 'info';
    }

    self::$level($message);
  }

  /**
   * Gets the current log file contents.
   */
  public static function getContents(): string {
    if (!self::$log_file || !is_readable(self::$log_file)) {
      return '';
    }
    // phpcs:ignore
    return (string) file_get_contents(self::$log_file);
  }

  /**
   * Clears the current log file.
   */
  public static function clear(): void {
    if (self::$log_file && file_exists(self::$log_file)) {
      // phpcs:ignore
      @unlink(self::$log_file);
    }

    self::$log_file = self::ensureLogFile();
  }

  /**
   * Writes a log entry.
   *
   * @param string $level Log level.
   * @param string $msg Message to log.
   * @param bool $mirror_error_log Whether to mirror to PHP error log.
   */
  private static function write(string $level, string $msg, bool $mirror_error_log = false): void {
    if (self::belowThreshold($level)) {
      return;
    }

    if (!self::$log_file) {
      return;
    }

    $context = self::resolveCallerContext();
    $clean_message = trim(str_replace(["\r", "\n"], ' ', $msg));

    $entry = sprintf(
      "%s [%s] [%s] %s%s\n",
      gmdate('Y-m-d H:i:s'),
      $level,
      $context['caller'],
      $clean_message,
      $context['file'] !== '' ? ' @ ' . $context['file'] . ':' . $context['line'] : ''
    );
    // phpcs:ignore
    file_put_contents(self::$log_file, $entry, FILE_APPEND | LOCK_EX);

    if (++self::$counter % self::ROTATE_CHECK_EVERY === 0) {
      self::maybeRotate();
    }

    if ($mirror_error_log) {
      // phpcs:ignore
      \error_log(sprintf(
        '%s [%s] [%s] %s%s',
        self::$plugin_name,
        $level,
        $context['caller'],
        $clean_message,
        $context['file'] !== '' ? ' @ ' . $context['file'] . ':' . $context['line'] : ''
      ));
    }

    if (self::$cli_output && defined('WP_CLI') && WP_CLI) {
      \WP_CLI::log(rtrim($entry));
    }
  }

  /**
   * Resolve the first real caller outside this logger.
   *
   * Returns:
   * - caller: Namespace\Class->method() or function()
   * - file: relative normalized path when possible
   * - line: line number or 0
   */
  private static function resolveCallerContext(): array {
    // phpcs:ignore
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, self::TRACE_LIMIT);

    foreach ($trace as $frame) {
      $class = isset($frame['class']) ? ltrim((string) $frame['class'], '\\') : '';
      $function = (string) ($frame['function'] ?? '');

      if ($class === __CLASS__) {
        continue;
      }

      if ($function === 'kimaai_log') {
        continue;
      }

      $caller = self::formatCaller($frame);
      $file = self::formatPath(isset($frame['file']) ? (string) $frame['file'] : '');
      $line = isset($frame['line']) ? (int) $frame['line'] : 0;

      return [
        'caller' => $caller,
        'file' => $file,
        'line' => $line,
      ];
    }

    return [
      'caller' => 'unknown()',
      'file' => '',
      'line' => 0,
    ];
  }

  /**
   * Formats a stack frame into a caller name.
   *
   * @param array $frame Stack frame.
   */
  private static function formatCaller(array $frame): string {
    $class = isset($frame['class']) ? ltrim((string) $frame['class'], '\\') : '';
    $type = isset($frame['type']) ? (string) $frame['type'] : '';
    $function = (string) ($frame['function'] ?? 'unknown');

    if ($class !== '') {
      return $class . $type . $function . '()';
    }

    return $function . '()';
  }

  /**
   * Formats a file path relative to WordPress when possible.
   *
   * @param string $path Absolute file path.
   */
  private static function formatPath(string $path): string {
    if ($path === '') {
      return '';
    }

    $path = wp_normalize_path($path);

    if (defined('ABSPATH')) {
      $abspath = wp_normalize_path(ABSPATH);
      if (str_starts_with($path, $abspath)) {
        return ltrim(substr($path, strlen($abspath)), '/');
      }
    }

    return $path;
  }

  /**
   * Rotates the log file when it exceeds the configured size.
   */
  private static function maybeRotate(): void {
    if (!self::$log_file || !file_exists(self::$log_file)) {
      return;
    }

    if (filesize(self::$log_file) <= self::MAX_SIZE_BYTES) {
      return;
    }

    $info = pathinfo(self::$log_file);
    $target = sprintf(
      '%s/%s_%s.%s',
      $info['dirname'],
      $info['filename'],
      gmdate('Y-m-d_H-i-s'),
      $info['extension']
    );

    // phpcs:ignore
    @rename(self::$log_file, $target);
    // phpcs:ignore
    file_put_contents(self::$log_file, '', LOCK_EX);

    self::info('Log rotated → ' . basename($target));
  }

  /**
   * Ensures a log file exists and returns its path.
   */
  private static function ensureLogFile(): ?string {
    $opt = get_option(self::$option_key);
    $opt = is_array($opt) ? $opt : [];

    if (!empty($opt['log_path']) && file_exists($opt['log_path'])) {
      return $opt['log_path'];
    }

    $uploads = wp_upload_dir();
    $dir = trailingslashit($uploads['basedir']);

    if (!is_dir($dir)) {
      wp_mkdir_p($dir);
    }

    $path = $dir . self::FILE_PREFIX . '_' . wp_generate_password(8, false) . '.log';
    // phpcs:ignore
    file_put_contents($path, '');
    $opt['log_path'] = $path;
    update_option(self::$option_key, $opt, false);

    return $path;
  }
}
