<?php

namespace Acms\Services\Update\Database;

/**
 * シーケンス採番テーブルの PRIMARY KEY を「ID カラム単体」へ張り替えるための、
 * 副作用を持たない SQL 組み立て・判定ロジック。
 *
 * 実際の DDL 実行・DB 接続・ログ出力は呼び出し側（Rule）が担い、本クラスは純粋関数のみを提供する。
 * これにより、トランザクションでロールバックできない DDL の挙動を直接実行せずに Unit テストで検証できる。
 *
 * テーブル名・カラム名はいずれも呼び出し側が内部定数（DB_PREFIX ＋ 固定名）として渡す前提であり、
 * ユーザー入力は含まない。
 */
class PrimaryKey
{
    /**
     * SHOW KEYS / SHOW INDEX の結果行から、PRIMARY KEY を構成するカラムを Seq_in_index 昇順で取り出す。
     *
     * @param array<array-key, mixed> $showKeysRows SHOW INDEX FROM `table` の結果行
     * @return list<string> PRIMARY KEY を構成するカラム名（順序つき）。PRIMARY KEY が無ければ空配列
     */
    public function extractPrimaryKeyColumns(array $showKeysRows): array
    {
        $primary = [];
        foreach ($showKeysRows as $row) {
            if (!is_array($row)) {
                continue;
            }
            if (!isset($row['Key_name']) || $row['Key_name'] !== 'PRIMARY') {
                continue;
            }
            $seq = isset($row['Seq_in_index']) ? (int) $row['Seq_in_index'] : 0;
            $primary[$seq] = (string) $row['Column_name'];
        }
        ksort($primary);

        return array_values($primary);
    }

    /**
     * すでに PRIMARY KEY が対象 ID カラム単体になっている（＝移行済み）かどうか。
     *
     * @param list<string> $primaryKeyColumns 現在の PRIMARY KEY 構成カラム
     * @param string $idColumn 対象 ID カラム名
     * @return bool 移行済みなら true
     */
    public function isAlreadyConverted(array $primaryKeyColumns, string $idColumn): bool
    {
        return $primaryKeyColumns === [$idColumn];
    }

    /**
     * 重複 ID 検出用の SELECT を組み立てる。
     * 1 件でも返れば「重複あり」と判断する想定（HAVING COUNT(*) > 1, LIMIT 1）。
     *
     * DB::query が連想配列を受け付けるのは v3.2 以降のため、アップデート処理から使えるよう
     * 文字列 SQL を返す（識別子は内部定数のみでユーザー入力を含まない）。
     *
     * @param string $table プレフィックス済みテーブル名
     * @param string $idColumn 対象 ID カラム名
     * @return string
     */
    public function buildDuplicateCheckSql(string $table, string $idColumn): string
    {
        return "SELECT `{$idColumn}` FROM `{$table}` GROUP BY `{$idColumn}` HAVING COUNT(*) > 1 LIMIT 1";
    }

    /**
     * PRIMARY KEY を対象 ID カラム単体へ張り替える ALTER 文を組み立てる。
     * 既存 PRIMARY KEY を持つテーブルは DROP してから ADD し、持たないテーブル（rule 等）は ADD のみ行う。
     *
     * DB::query が連想配列を受け付けるのは v3.2 以降のため、アップデート処理から使えるよう
     * 文字列 SQL を返す（識別子は内部定数のみでユーザー入力を含まない）。
     *
     * @param string $table プレフィックス済みテーブル名
     * @param string $idColumn 対象 ID カラム名
     * @param bool $hasExistingPrimaryKey 既存の PRIMARY KEY を持つか
     * @return string
     */
    public function buildConvertSql(string $table, string $idColumn, bool $hasExistingPrimaryKey): string
    {
        return $this->buildCompositeConvertSql($table, [$idColumn], $hasExistingPrimaryKey);
    }

    /**
     * PRIMARY KEY を指定カラム（単一・複合いずれも可）へ張り替える ALTER 文を組み立てる。
     * 既存 PRIMARY KEY を持つテーブルは DROP してから ADD し、持たないテーブルは ADD のみ行う。
     *
     * DB::query が連想配列を受け付けるのは v3.2 以降のため、アップデート処理から使えるよう
     * 文字列 SQL を返す（識別子は内部定数のみでユーザー入力を含まない）。
     *
     * @param string $table プレフィックス済みテーブル名
     * @param list<string> $idColumns 対象カラム名（複合キーは宣言順）
     * @param bool $hasExistingPrimaryKey 既存の PRIMARY KEY を持つか
     * @return string
     */
    public function buildCompositeConvertSql(string $table, array $idColumns, bool $hasExistingPrimaryKey): string
    {
        $quoted = [];
        foreach ($idColumns as $column) {
            $quoted[] = "`{$column}`";
        }

        $operations = [];
        if ($hasExistingPrimaryKey) {
            $operations[] = 'DROP PRIMARY KEY';
        }
        $operations[] = 'ADD PRIMARY KEY (' . implode(', ', $quoted) . ')';

        return "ALTER TABLE `{$table}` " . implode(', ', $operations);
    }
}
