<?php

declare(strict_types=1);

namespace StaticPHP\Registry;

use StaticPHP\Artifact\Artifact;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
use StaticPHP\Attribute\Artifact\BinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
use StaticPHP\Attribute\Artifact\CustomSource;
use StaticPHP\Attribute\Artifact\CustomSourceCheckUpdate;
use StaticPHP\Attribute\Artifact\SourceExtract;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Util\FileSystem;

class ArtifactLoader
{
    /** @var null|array<string, Artifact> Artifact instances */
    private static ?array $artifacts = null;

    public static function initArtifactInstances(): void
    {
        if (self::$artifacts !== null) {
            return;
        }
        foreach (ArtifactConfig::getAll() as $name => $item) {
            $artifact = new Artifact($name, $item);
            self::$artifacts[$name] = $artifact;
        }
    }

    public static function getArtifactInstance(string $artifact_name): ?Artifact
    {
        self::initArtifactInstances();
        return self::$artifacts[$artifact_name] ?? null;
    }

    /**
     * Load artifact definitions from PSR-4 directory.
     *
     * @param string $dir            Directory path
     * @param string $base_namespace Base namespace for dir's PSR-4 mapping
     * @param bool   $auto_require   Whether to auto-require PHP files (for external plugins not in autoload)
     */
    public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
    {
        self::initArtifactInstances();
        $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
        foreach ($classes as $class) {
            self::loadFromClass($class);
        }
    }

    public static function loadFromClass(string $class): void
    {
        $ref = new \ReflectionClass($class);

        $class_instance = $ref->newInstance();

        foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
            self::processCustomSourceAttribute($ref, $method, $class_instance);
            self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance);
            self::processCustomBinaryAttribute($ref, $method, $class_instance);
            self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance);
            self::processSourceExtractAttribute($ref, $method, $class_instance);
            self::processBinaryExtractAttribute($ref, $method, $class_instance);
            self::processAfterSourceExtractAttribute($ref, $method, $class_instance);
            self::processAfterBinaryExtractAttribute($ref, $method, $class_instance);
        }
    }

    /**
     * Get names of all loaded artifacts.
     *
     * @return string[]
     */
    public static function getLoadedArtifactNames(): array
    {
        self::initArtifactInstances();
        return array_keys(self::$artifacts ?? []);
    }

    /**
     * Process #[CustomSource] attribute.
     */
    private static function processCustomSourceAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(CustomSource::class);
        foreach ($attributes as $attribute) {
            /** @var CustomSource $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->setCustomSourceCallback([$class_instance, $method->getName()]);
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSource] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[CustomSourceCheckUpdate] attribute.
     */
    private static function processCustomSourceCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(CustomSourceCheckUpdate::class);
        foreach ($attributes as $attribute) {
            /** @var CustomSourceCheckUpdate $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->setCustomSourceCheckUpdateCallback([$class_instance, $method->getName()]);
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSourceCheckUpdate] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[CustomBinary] attribute.
     */
    private static function processCustomBinaryAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(CustomBinary::class);
        foreach ($attributes as $attribute) {
            /** @var CustomBinary $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                foreach ($instance->support_os as $os) {
                    self::$artifacts[$artifact_name]->setCustomBinaryCallback($os, [$class_instance, $method->getName()]);
                }
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinary] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[CustomBinaryCheckUpdate] attribute.
     */
    private static function processCustomBinaryCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(CustomBinaryCheckUpdate::class);
        foreach ($attributes as $attribute) {
            /** @var CustomBinaryCheckUpdate $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                foreach ($instance->support_os as $os) {
                    self::$artifacts[$artifact_name]->setCustomBinaryCheckUpdateCallback($os, [$class_instance, $method->getName()]);
                }
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinaryCheckUpdate] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[SourceExtract] attribute.
     * This attribute allows completely taking over the source extraction process.
     */
    private static function processSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(SourceExtract::class);
        foreach ($attributes as $attribute) {
            /** @var SourceExtract $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->setSourceExtractCallback([$class_instance, $method->getName()]);
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[SourceExtract] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[BinaryExtract] attribute.
     * This attribute allows completely taking over the binary extraction process.
     */
    private static function processBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(BinaryExtract::class);
        foreach ($attributes as $attribute) {
            /** @var BinaryExtract $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->setBinaryExtractCallback(
                    [$class_instance, $method->getName()],
                    $instance->platforms
                );
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[BinaryExtract] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[AfterSourceExtract] attribute.
     * This attribute registers a hook that runs after source extraction completes.
     */
    private static function processAfterSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(AfterSourceExtract::class);
        foreach ($attributes as $attribute) {
            /** @var AfterSourceExtract $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->addAfterSourceExtractCallback([$class_instance, $method->getName()]);
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterSourceExtract] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }

    /**
     * Process #[AfterBinaryExtract] attribute.
     * This attribute registers a hook that runs after binary extraction completes.
     */
    private static function processAfterBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
    {
        $attributes = $method->getAttributes(AfterBinaryExtract::class);
        foreach ($attributes as $attribute) {
            /** @var AfterBinaryExtract $instance */
            $instance = $attribute->newInstance();
            $artifact_name = $instance->artifact_name;
            if (isset(self::$artifacts[$artifact_name])) {
                self::$artifacts[$artifact_name]->addAfterBinaryExtractCallback(
                    [$class_instance, $method->getName()],
                    $instance->platforms
                );
            } else {
                throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterBinaryExtract] on '{$ref->getName()}::{$method->getName()}'");
            }
        }
    }
}
