<?php
/** @noinspection PhpLackOfCohesionInspection */
declare(strict_types=1);

namespace BeyondSEO\Domain\Integrations\WordPress\Seo\Entities\Optimiser\Base;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

use BeyondSEO\Domain\Integrations\WordPress\Seo\Entities\Optimiser\Base\Attributes\SeoMeta;
use BeyondSEO\Domain\Integrations\WordPress\Seo\Entities\Optimiser\Base\Enums\FactorStatus;
use BeyondSEO\Domain\Integrations\WordPress\Seo\Repo\InternalDB\Optimiser\InternalDBSeoFactor;
use BeyondSEO\Domain\Integrations\WordPress\Seo\Repo\InternalDB\Optimiser\InternalDBSeoOperations;
use BeyondSEO\Domain\Integrations\WordPress\Seo\Services\WPSeoOptimiserService;
use BeyondSEODeps\DDD\Domain\Base\Entities\Entity;
use BeyondSEODeps\DDD\Domain\Base\Entities\LazyLoad\LazyLoad;
use BeyondSEODeps\DDD\Domain\Base\Entities\LazyLoad\LazyLoadRepo;
use BeyondSEODeps\DDD\Infrastructure\Traits\Serializer\Attributes\HideProperty;
use RankingCoach\Inc\Core\Base\Traits\RcLoggerTrait;
use ReflectionClass;
use Throwable;

/**
 * Represents a SEO optimization factor within an analysis context.
 * 
 * @method WPSeoOptimiserService getService()
 * @property Factors $parent Collection of factors that this factor belongs to
 * @method Factors getParent()
 */
#[LazyLoadRepo(repoType: LazyLoadRepo::INTERNAL_DB, repoClass: InternalDBSeoFactor::class)]
class Factor extends Entity
{
    use RcLoggerTrait;

    // persistent properties
    /** @var int|null The unique identifier for this factor in the database */
    public ?int $id = 0;
    /** @var int|null The ID of the optimization context this factor belongs to */
    public ?int $contextId = 0;
    /** @var string|null The human-readable name of this factor */
    public ?string $factorName = null;
    /** @var string|null Unique identifier key for this specific factor type */
    public ?string $factorKey = null;
    /** @var string|null A description of this factor's purpose or function */
    public ?string $description = null;
    /** @var float The importance weight of this factor in the overall score calculation */
    public float $weight = 1;
    /** @var float The calculated score for this factor (0-1 scale) */
    public float $score = 0;

    /** @var array Data collected from content analysis for use in operations */
    #[HideProperty]
    public array $fetchedData = [];
    /** @var string Represents the status of a factor by score */
    public string $status = FactorStatus::MISSING;

    // additional properties
    /** @var int The ID of the WordPress post being analyzed */
    public int $postId;
    /** @var int|null The ID of the SEO analysis this factor is part of */
    public ?int $analysisId = 0;
    /** @var Operations Collection of operations executed by this factor */

    #[LazyLoad(repoType: LazyLoadRepo::INTERNAL_DB, repoClass: InternalDBSeoOperations::class)]
    public Operations $operations;

    /** @var FactorSuggestions Collection of improvement suggestions generated by this factor */
    public FactorSuggestions $suggestions;

    /** @var class-string[] List of operation classes to be loaded automatically */
    protected static array $operationsClasses = [];

    /**
     * Creates a new SEO factor instance with specified-key and weight.
     *
     * @param string|null $factoryName
     * @param string|null $factorKey Unique identifier for this factor type
     * @param float|null $weight Importance weight of this factor in the overall score (0-1 scale)
     * @param string|null $description A human-readable description of this factor's purpose
     * @param array|null $params Additional parameters for factor initialization
     * @param bool $initOperations Whether to initialize operations for this factor (default: false - used for lazyloading)
     */
    public function __construct(?string $factoryName = null, ?string $factorKey = null, ?float $weight = 1.0, ?string $description = null, ?array $params = [], bool $initOperations = false)
    {
        // Initialize the factor with default values
        parent::__construct();
        if (static::class === self::class) {
            return;
        }

        $this->postId = 0;
        $this->analysisId = 0;
        $this->contextId = 0;
        $this->factorName = $factoryName;
        $this->factorKey = $factorKey;
        $this->description = $description ?? null;
        $this->weight = ($weight !== null && $weight >= 0) ? $weight : 1.0;
        $this->suggestions = new FactorSuggestions();

        // Initialize operations and suggestions collections
        if ($initOperations) {
            $this->initOperations($params);
        }
    }

    /**
     * Loads operations for this factor. Operations are SEO checks that contribute to the factor's score.
     *
     * @param array $params Optional parameters to filter which operations to load
     * @return void
     */
    private function initOperations(array $params = []): void
    {
        $this->operations = new Operations();
        /** @var Operation $operationClass */
        foreach (static::$operationsClasses as $operationClass) {
            $attributes = (new ReflectionClass($operationClass))->getAttributes(SeoMeta::class);
            foreach ($attributes as $attribute) {
                /** @var SeoMeta $seoMeta */
                $seoMeta = $attribute->newInstance();
                // Check if the operation should be loaded based on the provided parameters
                if (isset($params['operation']) && !in_array($seoMeta->getKey('operation'), $params['operation'], true)) {
                    continue;
                }
                $operation = new $operationClass(
                    $seoMeta->getKey('operation'),
                    $seoMeta->getLocalizedName(),
                    $seoMeta->weight,
                );
                $this->addOperation($operation);
            }
        }
    }

    /**
     * Executes all operations for this factor and collects their improvement suggestions.
     * The process consists of:
     * 1. Fetching necessary data from the content
     * 2. Running each operation with the fetched data
     * 3. Collecting suggestions from each operation
     * 
     * @param array $parameters Configuration parameters including postId
     * @throws Throwable If an error occurs during operation execution
     */
    public function execute(array $parameters = []): void
    {
        // First, fetch all required data
        // Default implementation only extracts postId
        // Concrete factors should override this method with specific data-fetching logic
        $postId = $parameters['postId'] ?? 0;

        // Log warning about invalid post-ID
        if (($postId <= 0) && method_exists($this, 'log')) {
            $this->log("Invalid post ID provided for factor '$this->factorKey': $postId", 'DEBUG');
            return;
        }

        $this->postId = $postId;

        // Then execute all operations with access to the fetched data
        /** @var Operation $operation */
        foreach ($this->operations as $operation) {
            $operation->execute($this->postId);
            $operation->setScore();
            $operation->setSuggestions();
            $this->fetchedData[$operation->operationKey] = $operation->getResult();
            $this->collectSuggestionsFromOperation($operation);
        }
    }

    /**
     * Collects relevant improvement suggestions from an operation based on its results.
     * Each suggestion is filtered by the issue types reported by the operation and
     * added to the factor's suggestion collection if it matches.
     *
     * @param Operation $operation The operation to collect suggestions from
     * @throws Throwable If retrieving suggestions fails
     */
    protected function collectSuggestionsFromOperation(Operation $operation): void
    {
        // Get all possible suggestions defined for this operation type
        $operationSuggestions = $operation->getSuggestions();
        // Add each suggestion to the factor's suggestion collection if it matches the reported issues
        if ($operationSuggestions) {
            foreach ($operationSuggestions->elements as $suggestion) {
                $this->addFactorSuggestion($suggestion);
            }
        }
    }

    /**
     * Adds an improvement suggestion to this factor if not already present.
     * Prevents duplicate suggestions by checking operation key and title combinations.
     * 
     * @param FactorSuggestion $suggestion The SEO improvement suggestion to add
     */
    public function addFactorSuggestion(FactorSuggestion $suggestion): void
    {
        if (count($this->suggestions->getElements()) === 0) {
            $this->suggestions->add($suggestion);
            return;
        }

        foreach ($this->suggestions->elements as $suggestionElement) {
            /** we don't want to add the same issue */
            if ($suggestionElement->issueType === $suggestion->issueType) {
                continue;
            }
            $this->suggestions->add($suggestion);
        }
    }

    /**
     * Retrieves all SEO improvement suggestions collected for this factor.
     *
     * @return FactorSuggestions Collection of improvement suggestions for this factor
     * @throws Throwable
     */
    public function getFactorSuggestions(): FactorSuggestions
    {
        if (!isset($this->suggestions)) {
            $this->suggestions = new FactorSuggestions();
        }
        if (count($this->suggestions->getElements()) === 0 && count($this->operations->getElements()) > 0) {
            foreach ($this->operations as $operation) {
                $this->collectSuggestionsFromOperation($operation);
            }
        }
        return $this->suggestions->orderBy('priority');
    }

    /**
     * Calculates the overall score for this factor based on its operations.
     * Uses a weighted average of operation scores based on their importance.
     * 
     * @return float The calculated score for this factor (0-1 scale)
     */
    public function calculateScore(): float
    {
        // Use weighted score instead of average score to account for operation weights
        $this->score = $this->operations->getWeightedScore();
        $this->status = FactorStatus::fromScore($this->score);
        return $this->score;
    }

    /**
     * Adds an operation to this factor and associates it with this factor's analysis.
     * Operations are individual checks that contribute to the factor's overall score.
     * 
     * @param Operation $operation The SEO check operation to add
     * @return Operation The added operation instance
     */
    public function addOperation(Operation $operation): Operation
    {
        $operation->analysisId = $this->analysisId;
        $this->operations->add($operation);
        return $operation;
    }

    /**
     * Get a detailed score breakdown for this factor
     *
     * Returns the factor name, key, weight, calculated score and a list of
     * operation breakdowns used to build this score.
     */
    public function getScoreBreakdown(): array
    {
        $operations = [];
        foreach ($this->operations as $operation) {
            $operations[] = $operation->getScoreBreakdown();
        }

        return [
            'name' => $this->factorName,
            'key' => $this->factorKey,
            'weight' => $this->weight,
            'score' => $this->score,
            'operations' => $operations,
        ];
    }

    /**
     * Generates a unique identifier key for this factor.
     * Combines parent entity's unique key with factor-specific information.
     * 
     * @return string A unique string identifier for this factor instance
     */
    public function uniqueKey(): string
    {
        $parentUniqueKey = parent::uniqueKey();
        return $parentUniqueKey . '_' . md5($this->analysisId . $this->contextId . $this->factorKey);
    }

    /**
     * Check if this factor is available based on its operations
     * A factor is considered available if it has at least one available operation
     *
     * @return bool True if the factor has available operations, false otherwise
     */
    public function isAvailable(): bool
    {
        return isset($this->operations) && $this->operations->count() > 0;
    }
}