<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Logging;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop_runtime\DTO\LogVerbosity;
use Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionResult;
use Psr\Log\LoggerInterface;

/**
 * Centralized execution logging service.
 *
 * Handles all routine logging for node execution, controlled by verbosity
 * settings. This removes the need for individual processors to implement
 * their own logging for routine operations.
 *
 * The verbosity level is configured via flowdrop.settings config:
 * - Admin UI: /admin/flowdrop/settings
 * - Config: flowdrop.settings → logging.verbosity
 *
 * Can also be overridden programmatically via setVerbosity().
 */
class ExecutionLogger implements ExecutionLoggerInterface {

  /**
   * The logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private readonly LoggerInterface $logger;

  /**
   * Current verbosity level.
   *
   * @var \Drupal\flowdrop_runtime\DTO\LogVerbosity
   */
  private LogVerbosity $verbosity;

  /**
   * Constructs an ExecutionLogger.
   *
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory to read settings.
   */
  public function __construct(
    LoggerChannelFactoryInterface $loggerFactory,
    ConfigFactoryInterface $configFactory,
  ) {
    $this->logger = $loggerFactory->get("flowdrop_runtime");

    // Read verbosity from central FlowDrop config.
    $config = $configFactory->get('flowdrop.settings');
    $verbosityString = $config->get('logging.verbosity') ?? 'standard';
    $this->verbosity = LogVerbosity::fromString($verbosityString);
  }

  /**
   * {@inheritdoc}
   */
  public function setVerbosity(LogVerbosity $verbosity): static {
    $this->verbosity = $verbosity;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getVerbosity(): LogVerbosity {
    return $this->verbosity;
  }

  /**
   * {@inheritdoc}
   */
  public function logNodeStart(
    string $executionId,
    string $nodeId,
    string $nodeType,
  ): void {
    if (!$this->verbosity->shouldLog(LogVerbosity::Standard)) {
      return;
    }

    $this->logger->info("Executing node @node_id of type @node_type", [
      "@node_id" => $nodeId,
      "@node_type" => $nodeType,
      "@execution_id" => $executionId,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function logParameters(
    string $executionId,
    string $nodeId,
    ParameterBagInterface $params,
  ): void {
    if ($this->verbosity->shouldLog(LogVerbosity::Debug)) {
      // Full parameter dump at DEBUG level.
      $this->logger->debug("Node @node_id parameters: @params", [
        "@node_id" => $nodeId,
        "@params" => json_encode($params->toArray(), JSON_PRETTY_PRINT),
        "@execution_id" => $executionId,
      ]);
    }
    elseif ($this->verbosity->shouldLog(LogVerbosity::Verbose)) {
      // Summary at VERBOSE level.
      $paramArray = $params->toArray();
      $this->logger->info("Node @node_id received @count parameters", [
        "@node_id" => $nodeId,
        "@count" => count($paramArray),
        "@keys" => implode(", ", array_keys($paramArray)),
        "@execution_id" => $executionId,
      ]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function logNodeComplete(
    string $executionId,
    NodeExecutionResult $result,
  ): void {
    if (!$this->verbosity->shouldLog(LogVerbosity::Standard)) {
      return;
    }

    $nodeId = $result->getNodeId();
    $executionTime = $result->getExecutionTime();

    if ($this->verbosity->shouldLog(LogVerbosity::Debug)) {
      // Full output dump at DEBUG level.
      $output = $result->getOutput();
      $this->logger->info("Node @node_id completed in @time seconds. Output: @output", [
        "@node_id" => $nodeId,
        "@time" => round($executionTime, 3),
        "@output" => json_encode($output->toArray(), JSON_PRETTY_PRINT),
        "@execution_id" => $executionId,
      ]);
    }
    elseif ($this->verbosity->shouldLog(LogVerbosity::Verbose)) {
      // Output summary at VERBOSE level.
      $output = $result->getOutput();
      $outputArray = $output->toArray();
      $outputSize = strlen(serialize($outputArray));

      $this->logger->info("Node @node_id completed in @time seconds. Output: @count keys, @size bytes", [
        "@node_id" => $nodeId,
        "@time" => round($executionTime, 3),
        "@count" => count($outputArray),
        "@size" => $outputSize,
        "@execution_id" => $executionId,
      ]);
    }
    else {
      // Basic completion at STANDARD level.
      $this->logger->info("Node @node_id executed successfully in @time seconds", [
        "@node_id" => $nodeId,
        "@time" => round($executionTime, 3),
        "@execution_id" => $executionId,
      ]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function logValidation(
    string $executionId,
    string $nodeId,
    ValidationResult $result,
  ): void {
    // Always log validation failures at STANDARD level.
    if (!$result->isValid()) {
      $errorMessages = $result->getErrorMessages();
      $this->logger->warning("Node @node_id validation failed: @errors", [
        "@node_id" => $nodeId,
        "@errors" => implode("; ", $errorMessages),
        "@error_count" => $result->getErrorCount(),
        "@execution_id" => $executionId,
      ]);
    }

    // Log warnings at VERBOSE level.
    if ($result->hasWarnings() && $this->verbosity->shouldLog(LogVerbosity::Verbose)) {
      $warningMessages = $result->getWarningMessages();
      $this->logger->notice("Node @node_id validation warnings: @warnings", [
        "@node_id" => $nodeId,
        "@warnings" => implode("; ", $warningMessages),
        "@warning_count" => $result->getWarningCount(),
        "@execution_id" => $executionId,
      ]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function logNodeFailure(
    string $executionId,
    string $nodeId,
    \Throwable $exception,
    float $executionTime,
  ): void {
    // Always log failures regardless of verbosity.
    $this->logger->error("Node @node_id execution failed after @time seconds: @error", [
      "@node_id" => $nodeId,
      "@time" => round($executionTime, 3),
      "@error" => $exception->getMessage(),
      "@exception_class" => get_class($exception),
      "@execution_id" => $executionId,
    ]);

    // At DEBUG level, include stack trace.
    if ($this->verbosity->shouldLog(LogVerbosity::Debug)) {
      $this->logger->debug("Node @node_id stack trace: @trace", [
        "@node_id" => $nodeId,
        "@trace" => $exception->getTraceAsString(),
        "@execution_id" => $executionId,
      ]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function logNodeSkipped(
    string $executionId,
    string $nodeId,
    string $nodeType,
    string $reason,
  ): void {
    if (!$this->verbosity->shouldLog(LogVerbosity::Standard)) {
      return;
    }

    $this->logger->info("Skipping node @node_id (@node_type): @reason", [
      "@node_id" => $nodeId,
      "@node_type" => $nodeType,
      "@reason" => $reason,
      "@execution_id" => $executionId,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function logWorkflowStart(
    string $executionId,
    string $workflowId,
    int $nodeCount,
  ): void {
    if (!$this->verbosity->shouldLog(LogVerbosity::Standard)) {
      return;
    }

    $this->logger->info("Starting workflow @workflow_id execution with @count nodes", [
      "@workflow_id" => $workflowId,
      "@count" => $nodeCount,
      "@execution_id" => $executionId,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function logWorkflowComplete(
    string $executionId,
    string $workflowId,
    int $executedNodes,
    int $skippedNodes,
    float $totalTime,
  ): void {
    if (!$this->verbosity->shouldLog(LogVerbosity::Standard)) {
      return;
    }

    $this->logger->info("Workflow @workflow_id completed in @time seconds. Executed: @executed, Skipped: @skipped", [
      "@workflow_id" => $workflowId,
      "@time" => round($totalTime, 3),
      "@executed" => $executedNodes,
      "@skipped" => $skippedNodes,
      "@execution_id" => $executionId,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function logWorkflowFailure(
    string $executionId,
    string $workflowId,
    \Throwable $exception,
  ): void {
    // Always log failures regardless of verbosity.
    $this->logger->error("Workflow @workflow_id execution failed: @error", [
      "@workflow_id" => $workflowId,
      "@error" => $exception->getMessage(),
      "@exception_class" => get_class($exception),
      "@execution_id" => $executionId,
    ]);

    // At DEBUG level, include stack trace.
    if ($this->verbosity->shouldLog(LogVerbosity::Debug)) {
      $this->logger->debug("Workflow @workflow_id stack trace: @trace", [
        "@workflow_id" => $workflowId,
        "@trace" => $exception->getTraceAsString(),
        "@execution_id" => $executionId,
      ]);
    }
  }

}
