<?php

declare(strict_types=1);

namespace App\Framework\MachineLearning\ModelManagement;

use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DI\Attributes\DefaultImplementation;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelType;
use App\Framework\MachineLearning\ModelManagement\Exceptions\ModelAlreadyExistsException;
use App\Framework\MachineLearning\ModelManagement\Exceptions\ModelNotFoundException;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Core\ValueObjects\Timestamp;

/**
 * Database Model Registry - Database-backed ML Model Storage
 *
 * Stores model metadata in PostgreSQL/MySQL with the following schema:
 * - ml_models: Main model metadata table
 * - Indexed by: model_name, version, type, environment, created_at
 *
 * Performance:
 * - Sub-10ms lookups via indexed queries
 * - Transaction support for atomic operations
 * - Optimized for read-heavy workloads
 *
 * Usage:
 * ```php
 * $registry = new DatabaseModelRegistry($connection);
 * $registry->register($metadata);
 * $model = $registry->getLatest('fraud-detector');
 * ```
 */
#[DefaultImplementation(ModelRegistry::class)]
final readonly class DatabaseModelRegistry implements ModelRegistry
{
    public function __construct(
        private ConnectionInterface $connection
    ) {}

    public function register(ModelMetadata $metadata): void
    {
        // Check if model already exists
        if ($this->exists($metadata->modelName, $metadata->version)) {
            throw ModelAlreadyExistsException::forModel($metadata->getModelId());
        }

        $query = SqlQuery::create(
            sql: <<<'SQL'
                INSERT INTO ml_models (
                    model_name, version, model_type, configuration,
                    performance_metrics, created_at, is_deployed,
                    environment, description
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            SQL,
            parameters: [
                $metadata->modelName,
                $metadata->version->toString(),
                $metadata->modelType->value,
                json_encode($metadata->configuration),
                json_encode($metadata->performanceMetrics),
                $metadata->createdAt->format('Y-m-d H:i:s'),
                $metadata->isDeployed() ? 1 : 0,  // Explicit boolean conversion
                $metadata->environment ?? '',      // Ensure string, not null
                $metadata->metadata['description'] ?? null,
            ]
        );

        $this->connection->execute($query);
    }

    public function get(string $modelName, Version $version): ?ModelMetadata
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT * FROM ml_models
                WHERE model_name = ? AND version = ?
                LIMIT 1
            SQL,
            parameters: [$modelName, $version->toString()]
        );

        $row = $this->connection->queryOne($query);

        return $row ? $this->hydrateModel($row) : null;
    }

    public function getLatest(string $modelName): ?ModelMetadata
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT * FROM ml_models
                WHERE model_name = ?
                ORDER BY created_at DESC
                LIMIT 1
            SQL,
            parameters: [$modelName]
        );

        $row = $this->connection->queryOne($query);

        return $row ? $this->hydrateModel($row) : null;
    }

    public function getAll(string $modelName): array
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT * FROM ml_models
                WHERE model_name = ?
                ORDER BY created_at DESC
            SQL,
            parameters: [$modelName]
        );

        $rows = $this->connection->query($query)->fetchAll();

        return array_map(fn($row) => $this->hydrateModel($row), $rows);
    }

    public function getByType(ModelType $type): array
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT * FROM ml_models
                WHERE model_type = ?
                ORDER BY created_at DESC
            SQL,
            parameters: [$type->value]
        );

        $rows = $this->connection->query($query)->fetchAll();

        return array_map(fn($row) => $this->hydrateModel($row), $rows);
    }

    public function getByEnvironment(string $environment): array
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT * FROM ml_models
                WHERE is_deployed = ? AND environment = ?
                ORDER BY created_at DESC
            SQL,
            parameters: [true, $environment]
        );

        $rows = $this->connection->query($query)->fetchAll();

        return array_map(fn($row) => $this->hydrateModel($row), $rows);
    }

    public function getProductionModels(): array
    {
        return $this->getByEnvironment('production');
    }

    public function update(ModelMetadata $metadata): void
    {
        // Check if model exists
        if (!$this->exists($metadata->modelName, $metadata->version)) {
            throw new ModelNotFoundException(
                "Model '{$metadata->getModelId()}' not found in registry"
            );
        }

        $query = SqlQuery::create(
            sql: <<<'SQL'
                UPDATE ml_models SET
                    configuration = ?,
                    performance_metrics = ?,
                    is_deployed = ?,
                    environment = ?,
                    description = ?
                WHERE model_name = ? AND version = ?
            SQL,
            parameters: [
                json_encode($metadata->configuration),
                json_encode($metadata->performanceMetrics),
                $metadata->isDeployed() ? 1 : 0,  // Explicit boolean conversion
                $metadata->environment ?? '',      // Ensure string, not null
                $metadata->metadata['description'] ?? null,
                $metadata->modelName,
                $metadata->version->toString(),
            ]
        );

        $this->connection->execute($query);
    }

    public function delete(string $modelName, Version $version): bool
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                DELETE FROM ml_models
                WHERE model_name = ? AND version = ?
            SQL,
            parameters: [$modelName, $version->toString()]
        );

        return $this->connection->execute($query) > 0;
    }

    public function exists(string $modelName, Version $version): bool
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT COUNT(*) as count FROM ml_models
                WHERE model_name = ? AND version = ?
            SQL,
            parameters: [$modelName, $version->toString()]
        );

        return (int) $this->connection->queryScalar($query) > 0;
    }

    public function getAllModelNames(): array
    {
        $query = SqlQuery::select('ml_models', ['DISTINCT model_name']);

        $rows = $this->connection->query($query)->fetchAll();

        return array_column($rows, 'model_name');
    }

    public function getVersionCount(string $modelName): int
    {
        $query = SqlQuery::create(
            sql: <<<'SQL'
                SELECT COUNT(*) as count FROM ml_models
                WHERE model_name = ?
            SQL,
            parameters: [$modelName]
        );

        return (int) $this->connection->queryScalar($query);
    }

    public function getTotalCount(): int
    {
        $query = SqlQuery::create(
            sql: 'SELECT COUNT(*) as count FROM ml_models',
            parameters: []
        );

        return (int) $this->connection->queryScalar($query);
    }

    public function clear(): void
    {
        $query = SqlQuery::create(
            sql: 'DELETE FROM ml_models',
            parameters: []
        );

        $this->connection->execute($query);
    }

    /**
     * Hydrate ModelMetadata from database row
     */
    private function hydrateModel(array $row): ModelMetadata
    {
        return new ModelMetadata(
            modelName         : $row['model_name'],
            modelType         : ModelType::from($row['model_type']),
            version           : Version::fromString($row['version']),
            configuration     : json_decode($row['configuration'], true) ?? [],
            performanceMetrics: json_decode($row['performance_metrics'], true) ?? [],
            createdAt         : Timestamp::fromDateTime(new \DateTimeImmutable($row['created_at'])),
            deployedAt        : $row['is_deployed'] && !empty($row['created_at'])
                ? Timestamp::fromDateTime(new \DateTimeImmutable($row['created_at']))
                : null,
            environment       : $row['environment'],
            metadata          : $row['description'] ? ['description' => $row['description']] : []
        );
    }
}
