<?php

declare(strict_types=1);

namespace App\Framework\Database\Indexing;

use App\Framework\Database\Indexing\ValueObjects\IndexName;
use App\Framework\Database\Indexing\ValueObjects\IndexUsageMetrics;

/**
 * Detects unused or rarely used indexes that can be removed
 *
 * Helps identify indexes that waste storage and slow down writes
 */
final readonly class UnusedIndexDetector
{
    public function __construct(
        private IndexAnalyzer $analyzer,
        private IndexUsageTracker $usageTracker
    ) {}

    /**
     * Find all unused indexes on a table
     *
     * @param int $daysThreshold Number of days without usage to consider unused
     * @return array<array{
     *     index_name: string,
     *     table_name: string,
     *     columns: array<string>,
     *     last_used_days_ago: int,
     *     can_be_dropped: bool,
     *     reason: string
     * }>
     */
    public function findUnusedIndexes(string $tableName, int $daysThreshold = 30): array
    {
        $indexes = $this->analyzer->getTableIndexes($tableName);
        $unusedIndexes = [];

        foreach ($indexes as $index) {
            // Never suggest dropping PRIMARY or UNIQUE indexes
            if ($index['is_primary'] || $index['is_unique']) {
                continue;
            }

            $indexName = new IndexName($index['name']);
            $usage = $this->usageTracker->getUsageMetrics($indexName, $tableName);

            if (!$usage || $usage->isUnused($daysThreshold)) {
                $daysSinceLastUse = $usage ? $usage->getDaysSinceLastUse() : PHP_INT_MAX;

                $unusedIndexes[] = [
                    'index_name' => $index['name'],
                    'table_name' => $tableName,
                    'columns' => $index['columns'],
                    'last_used_days_ago' => $daysSinceLastUse,
                    'can_be_dropped' => true,
                    'reason' => $this->determineUnusedReason($usage, $daysThreshold)
                ];
            }
        }

        return $unusedIndexes;
    }

    /**
     * Find duplicate indexes (indexes with identical column coverage)
     *
     * @return array<array{
     *     duplicate_of: string,
     *     index_name: string,
     *     table_name: string,
     *     columns: array<string>,
     *     can_be_dropped: bool,
     *     reason: string
     * }>
     */
    public function findDuplicateIndexes(string $tableName): array
    {
        $indexes = $this->analyzer->getTableIndexes($tableName);
        $duplicates = [];
        $seen = [];

        foreach ($indexes as $index) {
            $columnKey = implode(',', $index['columns']);

            if (isset($seen[$columnKey])) {
                // This index duplicates an earlier one
                $original = $seen[$columnKey];

                // Don't suggest dropping PRIMARY or UNIQUE indexes
                if ($index['is_primary'] || $index['is_unique']) {
                    continue;
                }

                $duplicates[] = [
                    'duplicate_of' => $original['name'],
                    'index_name' => $index['name'],
                    'table_name' => $tableName,
                    'columns' => $index['columns'],
                    'can_be_dropped' => true,
                    'reason' => "Duplicate of index '{$original['name']}' on same columns"
                ];
            } else {
                $seen[$columnKey] = $index;
            }
        }

        return $duplicates;
    }

    /**
     * Find redundant indexes (where one index is a prefix of another)
     *
     * Example: idx_user_email is redundant if idx_user_email_status exists
     *
     * @return array<array{
     *     redundant_index: string,
     *     covered_by: string,
     *     table_name: string,
     *     columns: array<string>,
     *     can_be_dropped: bool,
     *     reason: string
     * }>
     */
    public function findRedundantIndexes(string $tableName): array
    {
        $indexes = $this->analyzer->getTableIndexes($tableName);
        $redundant = [];

        foreach ($indexes as $i => $index1) {
            foreach ($indexes as $j => $index2) {
                if ($i === $j) {
                    continue;
                }

                // Check if index1 is a prefix of index2
                if ($this->isPrefix($index1['columns'], $index2['columns'])) {
                    // Don't suggest dropping PRIMARY or UNIQUE indexes
                    if ($index1['is_primary'] || $index1['is_unique']) {
                        continue;
                    }

                    $redundant[] = [
                        'redundant_index' => $index1['name'],
                        'covered_by' => $index2['name'],
                        'table_name' => $tableName,
                        'columns' => $index1['columns'],
                        'can_be_dropped' => true,
                        'reason' => "Columns are prefix of index '{$index2['name']}'"
                    ];
                    break; // Only report once per redundant index
                }
            }
        }

        return $redundant;
    }

    /**
     * Get comprehensive unused index report for a table
     *
     * @return array{
     *     unused: array,
     *     duplicates: array,
     *     redundant: array,
     *     total_removable: int,
     *     estimated_space_savings: string
     * }
     */
    public function getUnusedIndexReport(string $tableName, int $daysThreshold = 30): array
    {
        $unused = $this->findUnusedIndexes($tableName, $daysThreshold);
        $duplicates = $this->findDuplicateIndexes($tableName);
        $redundant = $this->findRedundantIndexes($tableName);

        $totalRemovable = count($unused) + count($duplicates) + count($redundant);

        return [
            'unused' => $unused,
            'duplicates' => $duplicates,
            'redundant' => $redundant,
            'total_removable' => $totalRemovable,
            'estimated_space_savings' => $this->estimateSpaceSavings($totalRemovable)
        ];
    }

    /**
     * Generate DROP INDEX statements for unused indexes
     *
     * @return array<string> SQL statements to drop unused indexes
     */
    public function generateDropStatements(string $tableName, int $daysThreshold = 30): array
    {
        $report = $this->getUnusedIndexReport($tableName, $daysThreshold);
        $statements = [];

        foreach ($report['unused'] as $index) {
            if ($index['can_be_dropped']) {
                $statements[] = "DROP INDEX {$index['index_name']} ON {$tableName};";
            }
        }

        foreach ($report['duplicates'] as $index) {
            if ($index['can_be_dropped']) {
                $statements[] = "DROP INDEX {$index['index_name']} ON {$tableName};";
            }
        }

        foreach ($report['redundant'] as $index) {
            if ($index['can_be_dropped']) {
                $statements[] = "DROP INDEX {$index['redundant_index']} ON {$tableName};";
            }
        }

        return $statements;
    }

    /**
     * Determine reason for marking index as unused
     */
    private function determineUnusedReason(?IndexUsageMetrics $usage, int $daysThreshold): string
    {
        if (!$usage) {
            return 'Index has never been tracked or used';
        }

        if (!$usage->isUsed()) {
            return 'Index has never been used since tracking started';
        }

        $daysSinceLastUse = $usage->getDaysSinceLastUse();

        return "Index not used in last {$daysSinceLastUse} days (threshold: {$daysThreshold} days)";
    }

    /**
     * Check if array1 is a prefix of array2
     */
    private function isPrefix(array $array1, array $array2): bool
    {
        if (count($array1) >= count($array2)) {
            return false;
        }

        for ($i = 0; $i < count($array1); $i++) {
            if ($array1[$i] !== $array2[$i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Estimate space savings from dropping indexes
     */
    private function estimateSpaceSavings(int $indexCount): string
    {
        // Rough estimate: each index ~5-10% of table size
        // This is a simplified estimate
        if ($indexCount === 0) {
            return '0 MB';
        }

        $estimatedMB = $indexCount * 5;

        if ($estimatedMB < 1) {
            return '< 1 MB';
        }

        if ($estimatedMB > 1024) {
            $estimatedGB = round($estimatedMB / 1024, 2);
            return "{$estimatedGB} GB";
        }

        return "{$estimatedMB} MB";
    }
}
