<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\DTO\Snapshot;

/**
 * Result of validating a WorkflowSnapshot against a workflow definition.
 *
 * ValidationResult captures all validation errors and warnings found when
 * checking a snapshot's integrity. This includes dependency validation,
 * version matching, and node existence checks.
 *
 * Example usage:
 * @code
 * $result = $stateManager->validateSnapshot($snapshot, $workflowDefinition);
 *
 * if (!$result->isValid()) {
 *   foreach ($result->getErrors() as $error) {
 *     $this->logger->error($error);
 *   }
 * }
 *
 * // Check for specific validation categories
 * if ($result->hasVersionMismatch()) {
 *   // Handle version drift
 * }
 * @endcode
 *
 * @see \Drupal\flowdrop_runtime\Service\WorkflowState\WorkflowStateManagerInterface
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
final class ValidationResult implements \JsonSerializable {

  /**
   * Error code constants.
   */
  public const ERROR_VERSION_MISMATCH = "version_mismatch";
  public const ERROR_UNKNOWN_NODE = "unknown_node";
  public const ERROR_DEPENDENCY_NOT_MET = "dependency_not_met";
  public const ERROR_INVALID_STATUS = "invalid_status";
  public const ERROR_INVALID_TRANSITION = "invalid_transition";
  public const ERROR_MISSING_OUTPUT = "missing_output";
  public const ERROR_CIRCULAR_DEPENDENCY = "circular_dependency";

  /**
   * Warning code constants.
   */
  public const WARNING_ORPHAN_NODE = "orphan_node";
  public const WARNING_STALE_STATE = "stale_state";
  public const WARNING_INJECTED_WITHOUT_DEPENDENCY = "injected_without_dependency";

  /**
   * Constructs a new ValidationResult.
   *
   * @param bool $valid
   *   Whether the snapshot is valid (no errors).
   * @param array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}> $errors
   *   Array of validation errors.
   * @param array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}> $warnings
   *   Array of validation warnings (non-blocking issues).
   * @param array<string, mixed> $metadata
   *   Additional validation metadata.
   */
  public function __construct(
    public readonly bool $valid,
    public readonly array $errors = [],
    public readonly array $warnings = [],
    public readonly array $metadata = [],
  ) {}

  /**
   * Creates a valid result with no errors.
   *
   * @param array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}> $warnings
   *   Optional warnings.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return self
   *   A valid ValidationResult.
   */
  public static function valid(array $warnings = [], array $metadata = []): self {
    return new self(
      valid: TRUE,
      errors: [],
      warnings: $warnings,
      metadata: $metadata,
    );
  }

  /**
   * Creates an invalid result with errors.
   *
   * @param array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}> $errors
   *   The validation errors.
   * @param array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}> $warnings
   *   Optional warnings.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return self
   *   An invalid ValidationResult.
   */
  public static function invalid(
    array $errors,
    array $warnings = [],
    array $metadata = [],
  ): self {
    return new self(
      valid: FALSE,
      errors: $errors,
      warnings: $warnings,
      metadata: $metadata,
    );
  }

  /**
   * Creates an error entry.
   *
   * @param string $code
   *   The error code (use class constants).
   * @param string $message
   *   Human-readable error message.
   * @param string|null $nodeId
   *   The node ID related to this error, if applicable.
   * @param array<string, mixed> $details
   *   Additional error details.
   *
   * @return array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}
   *   The error entry.
   */
  public static function error(
    string $code,
    string $message,
    ?string $nodeId = NULL,
    array $details = [],
  ): array {
    $error = [
      "code" => $code,
      "message" => $message,
    ];

    if ($nodeId !== NULL) {
      $error["nodeId"] = $nodeId;
    }

    if (!empty($details)) {
      $error["details"] = $details;
    }

    return $error;
  }

  /**
   * Creates a warning entry.
   *
   * @param string $code
   *   The warning code (use class constants).
   * @param string $message
   *   Human-readable warning message.
   * @param string|null $nodeId
   *   The node ID related to this warning, if applicable.
   * @param array<string, mixed> $details
   *   Additional warning details.
   *
   * @return array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}
   *   The warning entry.
   */
  public static function warning(
    string $code,
    string $message,
    ?string $nodeId = NULL,
    array $details = [],
  ): array {
    $warning = [
      "code" => $code,
      "message" => $message,
    ];

    if ($nodeId !== NULL) {
      $warning["nodeId"] = $nodeId;
    }

    if (!empty($details)) {
      $warning["details"] = $details;
    }

    return $warning;
  }

  /**
   * Checks if the validation passed.
   *
   * @return bool
   *   TRUE if no errors were found.
   */
  public function isValid(): bool {
    return $this->valid;
  }

  /**
   * Checks if there are any errors.
   *
   * @return bool
   *   TRUE if there are errors.
   */
  public function hasErrors(): bool {
    return !empty($this->errors);
  }

  /**
   * Checks if there are any warnings.
   *
   * @return bool
   *   TRUE if there are warnings.
   */
  public function hasWarnings(): bool {
    return !empty($this->warnings);
  }

  /**
   * Gets all errors.
   *
   * @return array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}>
   *   The validation errors.
   */
  public function getErrors(): array {
    return $this->errors;
  }

  /**
   * Gets all warnings.
   *
   * @return array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}>
   *   The validation warnings.
   */
  public function getWarnings(): array {
    return $this->warnings;
  }

  /**
   * Gets error messages as a simple array of strings.
   *
   * @return array<string>
   *   Error messages.
   */
  public function getErrorMessages(): array {
    return array_map(fn($e) => $e["message"], $this->errors);
  }

  /**
   * Gets warning messages as a simple array of strings.
   *
   * @return array<string>
   *   Warning messages.
   */
  public function getWarningMessages(): array {
    return array_map(fn($w) => $w["message"], $this->warnings);
  }

  /**
   * Gets errors by code.
   *
   * @param string $code
   *   The error code to filter by.
   *
   * @return array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}>
   *   Errors matching the code.
   */
  public function getErrorsByCode(string $code): array {
    return array_filter($this->errors, fn($e) => $e["code"] === $code);
  }

  /**
   * Gets errors related to a specific node.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return array<array{code: string, message: string, nodeId?: string, details?: array<string, mixed>}>
   *   Errors related to the node.
   */
  public function getErrorsByNode(string $nodeId): array {
    return array_filter(
      $this->errors,
      fn($e) => isset($e["nodeId"]) && $e["nodeId"] === $nodeId
    );
  }

  /**
   * Checks if there's a version mismatch error.
   *
   * @return bool
   *   TRUE if version mismatch was detected.
   */
  public function hasVersionMismatch(): bool {
    return !empty($this->getErrorsByCode(self::ERROR_VERSION_MISMATCH));
  }

  /**
   * Checks if there are unknown node errors.
   *
   * @return bool
   *   TRUE if unknown nodes were detected.
   */
  public function hasUnknownNodes(): bool {
    return !empty($this->getErrorsByCode(self::ERROR_UNKNOWN_NODE));
  }

  /**
   * Checks if there are dependency errors.
   *
   * @return bool
   *   TRUE if dependency issues were detected.
   */
  public function hasDependencyErrors(): bool {
    return !empty($this->getErrorsByCode(self::ERROR_DEPENDENCY_NOT_MET));
  }

  /**
   * Gets the total count of errors.
   *
   * @return int
   *   Number of errors.
   */
  public function getErrorCount(): int {
    return count($this->errors);
  }

  /**
   * Gets the total count of warnings.
   *
   * @return int
   *   Number of warnings.
   */
  public function getWarningCount(): int {
    return count($this->warnings);
  }

  /**
   * Merges this result with another ValidationResult.
   *
   * @param self $other
   *   The other result to merge.
   *
   * @return self
   *   A new merged ValidationResult.
   */
  public function merge(self $other): self {
    return new self(
      valid: $this->valid && $other->valid,
      errors: array_merge($this->errors, $other->errors),
      warnings: array_merge($this->warnings, $other->warnings),
      metadata: array_merge($this->metadata, $other->metadata),
    );
  }

  /**
   * Creates a new result with an additional error.
   *
   * @param string $code
   *   The error code.
   * @param string $message
   *   The error message.
   * @param string|null $nodeId
   *   Optional node ID.
   * @param array<string, mixed> $details
   *   Optional details.
   *
   * @return self
   *   A new ValidationResult with the error added.
   */
  public function withError(
    string $code,
    string $message,
    ?string $nodeId = NULL,
    array $details = [],
  ): self {
    $errors = $this->errors;
    $errors[] = self::error($code, $message, $nodeId, $details);

    return new self(
      valid: FALSE,
      errors: $errors,
      warnings: $this->warnings,
      metadata: $this->metadata,
    );
  }

  /**
   * Creates a new result with an additional warning.
   *
   * @param string $code
   *   The warning code.
   * @param string $message
   *   The warning message.
   * @param string|null $nodeId
   *   Optional node ID.
   * @param array<string, mixed> $details
   *   Optional details.
   *
   * @return self
   *   A new ValidationResult with the warning added.
   */
  public function withWarning(
    string $code,
    string $message,
    ?string $nodeId = NULL,
    array $details = [],
  ): self {
    $warnings = $this->warnings;
    $warnings[] = self::warning($code, $message, $nodeId, $details);

    return new self(
      valid: $this->valid,
      errors: $this->errors,
      warnings: $warnings,
      metadata: $this->metadata,
    );
  }

  /**
   * Converts the result to an array.
   *
   * @return array<string, mixed>
   *   The result as an array.
   */
  public function toArray(): array {
    return [
      "valid" => $this->valid,
      "errors" => $this->errors,
      "warnings" => $this->warnings,
      "metadata" => $this->metadata,
      "errorCount" => $this->getErrorCount(),
      "warningCount" => $this->getWarningCount(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function jsonSerialize(): mixed {
    return $this->toArray();
  }

  /**
   * Returns a string summary of the validation result.
   *
   * @return string
   *   A human-readable summary.
   */
  public function __toString(): string {
    if ($this->valid) {
      $msg = "Validation passed";
      if ($this->hasWarnings()) {
        $msg .= " with " . $this->getWarningCount() . " warning(s)";
      }
      return $msg;
    }

    return "Validation failed with " . $this->getErrorCount() . " error(s)";
  }

}
