<?php
/**
 * @package     MefisAssetBridge
 * @subpackage  MefisAssetBridgeCore_Api
 *
 * @copyright   Copyright (C) 2025 Goran Usljebrka. All rights reserved.
 * @license     Proprietary — No resale or incorporation into other projects without explicit written permission. see LICENSE.txt
 */

namespace MefisAssetBridgeCore\Api;

use MefisAssetBridgeCore\Api\ApiClient;
use MefisAssetBridgeCore\Auth\KeyStore;
use MefisAssetBridgeCore\Cache\CoreCache;
use MefisAssetBridgeCore\Api\Endpoints\Login;
use MefisAssetBridgeCore\Api\Endpoints\Reset;
use MefisAssetBridgeCore\Api\Endpoints\Register;
use MefisAssetBridgeCore\Auth\AuthenticatedUser;
use MefisAssetBridgeCore\Api\Endpoints\Handshake;
use MefisAssetBridgeCore\Api\Endpoints\DeleteApiKey;
use MefisAssetBridgeCore\Api\Endpoints\RefreshToken;
use MefisAssetBridgeCore\Api\Endpoints\Asset\GetAsset;
use MefisAssetBridgeCore\Interfaces\EndpointInterface;
use MefisAssetBridgeCore\Api\Endpoints\Asset\GetAssets;
use MefisAssetBridgeCore\Api\Endpoints\Asset\SaveAsset;
use MefisAssetBridgeCore\Api\Endpoints\GetActiveApiKeys;
use MefisAssetBridgeCore\Api\Endpoints\Asset\DeleteAsset;
use MefisAssetBridgeCore\Api\Endpoints\Asset\RenameAsset;
use MefisAssetBridgeCore\Api\Endpoints\Asset\GetAssetData;
use MefisAssetBridgeCore\Api\Endpoints\Asset\GetAssetThumbnail;
use MefisAssetBridgeCore\Api\Endpoints\Asset\ChangeAssetCategory;
use MefisAssetBridgeCore\Api\Endpoints\Asset\ChangeAssetThumbnail;
use MefisAssetBridgeCore\Log;
/**
 * API Factory class
 */
class ApiFactory
{
    /** @var string API base URL */
    private string $baseUrl;

    /** @var string|null UUID */
    private ?string $uuid = null;

    /**
     * Directory from which UUID dotfiles are loaded and written.
     * Injected by the CMS layer so that UUID files survive plugin updates.
     * Falls back to __DIR__/../ (relative to core/Api/) when empty.
     *
     * @var string
     */
    private string $uuidDir = '';

    /** @var string Path hash */
    private string $pathHash;

    /** @var ApiClient API client instance */
    private ApiClient $_apiClient;

    /** @var AuthenticatedUser|null Authenticated user */
    private ?AuthenticatedUser $user;

    /** @var string Master Key to encode asset data */
    private string $masterKey;

    /** @var bool Whether to verify SSL certificates */
    private bool $verifySSL;

    /** @var CoreCache|null Cache instance injected by the CMS layer */
    private ?CoreCache $cache = null;

    /** @var array Map of endpoint names to classes */
    private array $endpointMap = [
        'handshake'        => Handshake::class,
        'login'            => Login::class,
        'reset'            => Reset::class,
        'register'         => Register::class,
        'refreshtoken'     => RefreshToken::class,
        'getactiveapikeys' => GetActiveApiKeys::class,
        'deleteapikey'     => DeleteApiKey::class,
        'getasset'         => GetAsset::class,
        'getassetdata'     => GetAssetData::class,
        'getassets'        => GetAssets::class,
        'getassetthumbnail' => GetAssetThumbnail::class,
        'saveasset'        => SaveAsset::class,
        'renameasset'      => RenameAsset::class,
        'changeassetcategory' => ChangeAssetCategory::class,
        'changeassetthumbnail' => ChangeAssetThumbnail::class,
        'deleteasset'      => DeleteAsset::class,
    ];

    /**
     * Singleton instance
     *
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * Private constructor to enforce singleton usage. Use getInstance() to obtain
     * the shared ApiFactory.
     *
     * @param string $baseUrl
     * @param AuthenticatedUser|null $user
     */
    private function __construct(string $baseUrl = '', ?AuthenticatedUser $user = null)
    {
        $this->baseUrl = $baseUrl;
        $this->_apiClient = new ApiClient($this->baseUrl);
        $this->user    = $user;
        $this->pathHash   = hash('sha256', __DIR__);
        $this->masterKey  = '';
        $this->verifySSL  = true; // Default to SSL verification enabled
    }

    /**
     * Get the shared ApiFactory instance. Optionally allows passing a base URL and
     * user on the first call; subsequent calls will return the same object and
     * ignore parameters.
     *
     * @param string $baseUrl
     * @param AuthenticatedUser|null $user
     * @return self
     */
    public static function getInstance(string $baseUrl = '', ?AuthenticatedUser $user = null): self
    {
        if (self::$instance === null) {
            self::$instance = new self($baseUrl, $user);
        }

        return self::$instance;
    }

    public function getWebsiteId(): ?string
    {
        return $this->uuid;
    }

    public function getBaseUrl(): string
    {
        return $this->baseUrl;
    }

    private function loadUuid(): void
    {
        $filename = $this->buildUUIDName($this->baseUrl);
        $uuidPath = $this->resolveUuidPath($filename);

        if (file_exists($uuidPath)) {
            $this->uuid = trim(file_get_contents($uuidPath));
        }
        $this->_apiClient->setUuid($this->uuid ?? '');
    }

    public function saveUuid(string $uuid): void
    {
        $filename = $this->buildUUIDName($this->baseUrl);
        $uuidPath = $this->resolveUuidPath($filename);

        if (file_put_contents($uuidPath, $uuid) === false) {
            throw new \RuntimeException("Unable to write UUID to file: " . $uuidPath);
        }
        @chmod($uuidPath, 0600);
        $this->uuid = $uuid;
    }

    /**
     * Resolve the full path for a UUID dotfile.
     *
     * Uses the injected $uuidDir when available; falls back to the legacy
     * path relative to core/Api/ for non-WordPress CMS environments.
     *
     * @param string $filename  Hash-based filename without the leading dot or .uuid suffix.
     * @return string           Absolute file path.
     */
    private function resolveUuidPath(string $filename): string
    {
        if ($this->uuidDir !== '') {
            return rtrim($this->uuidDir, '/\\') . DIRECTORY_SEPARATOR . ".{$filename}.uuid";
        }

        return __DIR__ . "/../.{$filename}.uuid";
    }

    /**
     * Set the directory used for reading and writing UUID dotfiles.
     *
     * Must be called before setBaseUrl(), which triggers loadUuid() internally.
     *
     * @param string $dir Absolute path to the directory (trailing separator optional).
     * @return self
     */
    public function setUuidDir(string $dir): self
    {
        $this->uuidDir = $dir;
        return $this;
    }

    /**
     * Set API base URL
     *
     * @param string $baseUrl API base URL
     * @return self
     */
    public function setBaseUrl(string $baseUrl): self
    {
        $this->baseUrl = $baseUrl;
        $this->_apiClient->setBaseUrl($this->baseUrl);

        // load uuid
        $this->loadUuid();
        return $this;
    }
    /**
     * Set SSL check flag
     *
     * @param bool $sslCheck Whether to check SSL certificate
     * @return self
     */
    public function setSSLCheck(bool $sslCheck): self
    {
        $this->_apiClient->setSSLCheck($sslCheck);
        $this->verifySSL = $sslCheck; // Also set for S3Storage
        return $this;
    }
    /**
     * Set component version
     *
     * @param string $version Component version
     * @return self
     */
    public function setComponentVersion(string $version): self
    {
        $this->_apiClient->setComponentVersion($version);
        return $this;
    }

    /**
     * Set authenticated user
     *
     * @param AuthenticatedUser|null $user Authenticated user
     * @return self
     */
    public function setUser(?AuthenticatedUser $user): self
    {
        $this->user = $user;
        $this->_apiClient->setUser($user);
        return $this;
    }

    public function createEndpoint(string $endpointName)
    {
        $endpointKey = strtolower($endpointName);

        if (! isset($this->endpointMap[$endpointKey])) {
            throw new \InvalidArgumentException("Unknown endpoint: $endpointName");
        }

        $className   = $this->endpointMap[$endpointKey];
        $reflection  = new \ReflectionClass($className);
        $constructor = $reflection->getConstructor();

        // No constructor -> instantiate directly
        if ($constructor === null || $constructor->getNumberOfParameters() === 0) {
            return $reflection->newInstance();
        }

        $args = [];

        foreach ($constructor->getParameters() as $param) {
            $resolved      = false;
            $paramName     = $param->getName();
            $paramType     = $param->getType();
            $resolvedValue = null;

            // Determine a candidate class type name (handle union types on PHP 8+)
            $candidateClass = null;
            if ($paramType instanceof \ReflectionNamedType) {
                if (! $paramType->isBuiltin()) {
                    $candidateClass = $paramType->getName();
                }
            } elseif ($paramType instanceof \ReflectionUnionType) {
                foreach ($paramType->getTypes() as $t) {
                    if ($t instanceof \ReflectionNamedType  && ! $t->isBuiltin()) {
                        $candidateClass = $t->getName();
                        break;
                    }
                }
            }

            // Resolve by class type hints
            if ($candidateClass !== null) {
                // ApiClient
                if ($candidateClass === ApiClient::class || is_a($this->_apiClient, $candidateClass, true)) {
                    $resolvedValue = $this->_apiClient;
                    $resolved      = true;
                } elseif ($candidateClass === AuthenticatedUser::class) {
                    $resolvedValue = $this->user;
                    $resolved      = true;
                } elseif ($candidateClass === CoreCache::class) {
                    $resolvedValue = $this->cache; // null when CMS hasn't injected one
                    $resolved      = true;
                } else {
                    // expand if needed..
                }
            }

            if (! $resolved) {
                // Resolve common scalar/builtin parameter names
                switch (strtolower($paramName)) {
                    case 'uuid':
                    case 'websiteid':
                    case 'website_id':
                        $resolvedValue = $this->uuid;
                        $resolved      = true;
                        break;

                    case 'apiclient':
                        $resolvedValue = $this->_apiClient;
                        $resolved      = true;
                        break;

                    case 'user':
                    case 'authenticateduser':
                        $resolvedValue = $this->user;
                        $resolved      = true;
                        break;

                    case 'baseurl':
                    case 'base_url':
                        $resolvedValue = $this->baseUrl;
                        $resolved      = true;
                        break;

                    case 'pathhash':
                    case 'path':
                        $resolvedValue = $this->pathHash;
                        $resolved      = true;
                        break;

                    case 'masterkey':
                    case 'master_key':
                        $resolvedValue = $this->masterKey;
                        $resolved      = true;
                        break;

                    case 'verifyssl':
                        $resolvedValue = $this->verifySSL;
                        $resolved      = true;
                        break;

                    case 'cache':
                    case 'corecache':
                        $resolvedValue = $this->cache;
                        $resolved      = true;
                        break;

                    // Add more name-based cases here as needed
                }
            }

            // Fallbacks
            if (! $resolved) {
                if ($param->isDefaultValueAvailable()) {
                    $resolvedValue = $param->getDefaultValue();
                    $resolved      = true;
                } elseif ($param->allowsNull()) {
                    $resolvedValue = null;
                    $resolved      = true;
                }
            }

            if (! $resolved) {
                throw new \RuntimeException(sprintf(
                    'Unable to resolve constructor parameter "%s" for endpoint %s',
                    $paramName,
                    $className
                ));
            }

            $args[] = $resolvedValue;
        }

        try {
            return $reflection->newInstanceArgs($args);
        } catch (\Throwable $e) {
            $log = Log::getInstance();
            $log->error("Failed to instantiate endpoint $endpointName: " . $e->getMessage(), 'api-factory');
            throw new \RuntimeException("Failed to initiate API endpoint.", 500);
        }
    }

    /**
     * Magic method to create endpoints dynamically
     *
     * @param string $name Method name
     * @param array $arguments Method arguments
     * @return mixed
     * @throws \BadMethodCallException
     */
    public function __call(string $name, array $arguments)
    {
        // Check if the method is a create*Endpoint method
        if (preg_match('/^create(.+)Endpoint$/', $name, $matches)) {
            $endpointType = lcfirst($matches[1]);
            return $this->createEndpoint($endpointType);
        }

        throw new \BadMethodCallException("Unknown method: $name");
    }

    public function setDeviceInfo(string $str): self
    {
        $this->_apiClient->setDeviceInfo($str);
        return $this;
    }

    public function setDomain(string $str): self
    {
        $this->_apiClient->setDomain($str);
        return $this;
    }

    public function setCMSUserId(int $id): self
    {
        $this->_apiClient->setCMSUserId($id);
        return $this;
    }

    public function setCMSVersion(string $version): self
    {
        $this->_apiClient->setCMSVersion($version);
        return $this;
    }

    public function setCMSType(string $type): self
    {
        $this->_apiClient->setCMSType($type);
        return $this;
    }

    public function setMasterKey(string $key): self
    {
        $this->masterKey = $key;
        return $this;
    }

    /**
     * Provide a CoreCache instance to endpoints that support caching.
     * The cache directory is defined by the CMS layer before calling this.
     *
     * @param CoreCache $cache
     * @return self
     */
    public function setCache(CoreCache $cache): self
    {
        $this->cache = $cache;
        return $this;
    }

    public function setKeystore(KeyStore $KeyStore): self
    {
        $this->_apiClient->setKeystore($KeyStore);
        return $this;
    }
    private function buildUUIDName(string $string): string
    {
        $url = trim($string);
        if (strpos($url, '://') === false) {
            $url = 'http://' . $url;
        }

        $parts = parse_url($url);
        $host  = isset($parts['host']) ? strtolower($parts['host']) : '';
        $port  = isset($parts['port']) ? ':' . $parts['port'] : '';
        $path  = isset($parts['path']) ? rtrim($parts['path'], "/") : '';
        $query = isset($parts['query']) ? '?' . $parts['query'] : '';

        $normalized = $host . $port . $path . $query;
        return substr(hash('sha256', $normalized), 0, 16);
    }
}
