<?php
$isCli = (PHP_SAPI === 'cli');

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/admin_jobs.php';
if (!$isCli) {
    requireAdmin();
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        header('Content-Type: application/json; charset=utf-8');
        http_response_code(405);
        echo json_encode(['success' => false, 'message' => '스캔은 POST 방식으로만 실행됩니다.'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    requireCsrfToken();
}

set_time_limit(0);
ini_set('display_errors', 0);

$scanStartedAt = date('Y-m-d H:i:s');
$startTime = microtime(true);
$totalFound = 0;
$newCount = 0;
$updatedCount = 0;
$deletedCount = 0;
$skippedCount = 0;
$errorCount = 0;
$tagCount = 0;
$filenameCount = 0;
$manualProtectedCount = 0;
$coverSuccessCount = 0;
$coverMissingCount = 0;
$metadataErrorCount = 0;
$encodingFixCount = 0;  // 인코딩 보정이 적용된 횟수
$message = '';
$adminJobId = null;

function scanLogMessage($msg, $level = 'INFO') {
    global $isCli;
    writeScanLog($msg, $level);
    if (function_exists('writeAppLog') && in_array(strtoupper((string)$level), ['WARNING', 'ERROR'], true)) {
        writeAppLog(strtoupper((string)$level) === 'ERROR' ? 'error' : 'warning', 'scan_error', $msg, ['level' => $level], __FILE__);
    }
    if ($isCli) echo sprintf("[%s] [%s] %s%s", date('H:i:s'), $level, $msg, PHP_EOL);
}

function scanMetadataParams($metadata, $prefix = '') {
    $text = static function ($value) {
        return $value === null ? null : ensureUtf8String($value);
    };
    return [
        $prefix . ':title' => $text($metadata['title'] ?? null),
        $prefix . ':artist' => $text($metadata['artist'] ?? null),
        $prefix . ':album' => $text($metadata['album'] ?? null),
        $prefix . ':album_artist' => $text($metadata['album_artist'] ?? null),
        $prefix . ':genre' => $text($metadata['genre'] ?? null),
        $prefix . ':year' => $text($metadata['year'] ?? null),
        $prefix . ':track_no' => $metadata['track_no'] ?? null,
        $prefix . ':disc_no' => $metadata['disc_no'] ?? null,
        $prefix . ':duration' => $metadata['duration'] ?? null,
        $prefix . ':composer' => $text($metadata['composer'] ?? null),
        $prefix . ':comment' => $text($metadata['comment'] ?? null),
        $prefix . ':bitrate' => $metadata['bitrate'] ?? null,
        $prefix . ':sample_rate' => $metadata['sample_rate'] ?? null,
        $prefix . ':channels' => $text($metadata['channels'] ?? null),
        $prefix . ':cover_hash' => $text($metadata['cover_hash'] ?? null),
        $prefix . ':cover_mime' => $text($metadata['cover_mime'] ?? null),
        $prefix . ':has_cover' => $metadata['has_cover'] ?? 0,
        $prefix . ':metadata_source' => $text($metadata['metadata_source'] ?? null),
        $prefix . ':metadata_error' => $text($metadata['metadata_error'] ?? null),
    ];
}

function scanNormalizePathForCompare($path) {
    $path = ensureUtf8String((string)$path);
    $path = str_replace('\\', '/', $path);
    $path = preg_replace('#/+#', '/', $path);
    $path = rtrim($path, '/');
    return function_exists('mb_strtolower') ? mb_strtolower($path, 'UTF-8') : strtolower($path);
}

function scanNormalizePathRoot($path) {
    $path = scanNormalizePathForCompare($path);
    return $path === '' ? '' : $path . '/';
}

function scanPathWithinRoot($path, $normalizedRoot) {
    if ($normalizedRoot === '') return false;
    return strpos(scanNormalizePathForCompare($path) . '/', $normalizedRoot) === 0;
}

try {
    $pdo = getDBConnection();
    $scanPath = getScanPathSetting();
    $allowedExtensions = array_values(array_unique(array_filter(array_map(
        static fn($ext) => strtolower(ltrim((string)$ext, '.')),
        getAllowedExtensionsSetting()
    ))));
    $refreshMetadata = settingBool('scan_refresh_metadata', false);
    $forceMetadata = settingBool('scan_force_metadata', false);
    $requestedScanMode = $isCli ? 'settings' : (string)($_POST['scan_mode'] ?? 'quick');
    if (!in_array($requestedScanMode, ['quick', 'metadata', 'force', 'settings'], true)) {
        $requestedScanMode = 'quick';
    }
    if ($requestedScanMode === 'quick') {
        $refreshMetadata = false;
        $forceMetadata = false;
    } elseif ($requestedScanMode === 'metadata') {
        $refreshMetadata = true;
        $forceMetadata = false;
    } elseif ($requestedScanMode === 'force') {
        $refreshMetadata = true;
        $forceMetadata = true;
    }
    $adminJobStart = startAdminJob('scan', '음악 스캔', 'scan:' . $requestedScanMode, [
        'scan_mode' => $requestedScanMode,
        'refresh_metadata' => $refreshMetadata,
        'force_metadata' => $forceMetadata,
    ], !$isCli);
    if (!$isCli && empty($adminJobStart['ok']) && !empty($adminJobStart['duplicate'])) {
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        http_response_code(409);
        echo json_encode([
            'success' => false,
            'message' => '이미 같은 스캔 작업이 진행 중입니다. 작업 큐에서 진행 상태를 확인하세요.',
            'job_id' => (int)($adminJobStart['id'] ?? 0),
        ], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        exit;
    }
    $adminJobId = (int)($adminJobStart['id'] ?? 0);
    $winScanPath = resolveFilesystemPath($scanPath, 'readable_dir') ?? toWinPath($scanPath);

    scanLogMessage('음악 스캔을 시작했습니다.');

    if (!is_dir($winScanPath)) {
        throw new Exception('스캔 폴더를 찾을 수 없습니다. 관리자 설정의 음악 폴더 경로를 확인하세요.');
    }

    $realWinScanPath = realpath($winScanPath) ?: $winScanPath;
    $utf8ScanRoot = rtrim(str_replace('\\', '/', toUtf8Path($realWinScanPath)), '/') . '/';
    $normalizedScanRoot = scanNormalizePathRoot($utf8ScanRoot);
    $foundHashes = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($winScanPath, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    $selectStmt = $pdo->prepare("
        SELECT id, file_size, file_mtime, is_missing, year, cover_hash, is_manual_edited,
               title, artist, album, genre
        FROM songs
        WHERE file_hash = :file_hash
    ");
    $insertStmt = $pdo->prepare("
        INSERT INTO songs (
            title, artist, album, album_artist, genre, year, track_no, disc_no, duration,
            composer, comment, bitrate, sample_rate, channels, file_path, file_hash,
            folder_path, folder_name, relative_path, file_size, file_mtime, extension, cover_hash, has_cover, cover_mime, metadata_source,
            metadata_updated_at, last_metadata_scan_at, metadata_error, is_manual_edited, is_missing
        ) VALUES (
            :title, :artist, :album, :album_artist, :genre, :year, :track_no, :disc_no, :duration,
            :composer, :comment, :bitrate, :sample_rate, :channels, :file_path, :file_hash,
            :folder_path, :folder_name, :relative_path, :file_size, :file_mtime, :extension, :cover_hash, :has_cover, :cover_mime, :metadata_source,
            NOW(), NOW(), :metadata_error, 0, 0
        )
    ");
    $updateFullStmt = $pdo->prepare("
        UPDATE songs
        SET title = :title,
            artist = :artist,
            album = :album,
            album_artist = :album_artist,
            genre = :genre,
            year = :year,
            track_no = :track_no,
            disc_no = :disc_no,
            duration = :duration,
            composer = :composer,
            comment = :comment,
            bitrate = :bitrate,
            sample_rate = :sample_rate,
            channels = :channels,
            file_path = :file_path,
            folder_path = :folder_path,
            folder_name = :folder_name,
            relative_path = :relative_path,
            file_size = :file_size,
            file_mtime = :file_mtime,
            extension = :extension,
            cover_hash = :cover_hash,
            has_cover = :has_cover,
            cover_mime = :cover_mime,
            cover_cache_path = NULL,
            metadata_source = :metadata_source,
            metadata_updated_at = NOW(),
            last_metadata_scan_at = NOW(),
            metadata_error = :metadata_error,
            is_missing = 0
        WHERE file_hash = :file_hash
    ");
    $updateTechnicalStmt = $pdo->prepare("
        UPDATE songs
        SET duration = :duration,
            bitrate = :bitrate,
            sample_rate = :sample_rate,
            channels = :channels,
            file_path = :file_path,
            folder_path = :folder_path,
            folder_name = :folder_name,
            relative_path = :relative_path,
            file_size = :file_size,
            file_mtime = :file_mtime,
            extension = :extension,
            cover_hash = :cover_hash,
            has_cover = :has_cover,
            cover_mime = :cover_mime,
            cover_cache_path = NULL,
            metadata_source = CASE WHEN metadata_source IS NULL OR metadata_source = '' THEN :metadata_source ELSE metadata_source END,
            last_metadata_scan_at = NOW(),
            metadata_updated_at = NOW(),
            metadata_error = :metadata_error,
            is_missing = 0
        WHERE file_hash = :file_hash
    ");
    $markActiveStmt = $pdo->prepare("UPDATE songs SET is_missing = 0 WHERE file_hash = :file_hash");

    foreach ($iterator as $fileInfo) {
        if (!$fileInfo->isFile()) continue;
        $winFilePath = $fileInfo->getPathname();
        $ext = strtolower(pathinfo($winFilePath, PATHINFO_EXTENSION));
        if (!in_array($ext, $allowedExtensions, true)) continue;

        $totalFound++;
        try {
            $realWinPath = realpath($winFilePath) ?: $winFilePath;
            $utf8FilePath = toUtf8Path($realWinPath);
            $fileHash = hash('sha256', $utf8FilePath);
            $foundHashes[$fileHash] = true;
            clearstatcache(true, $realWinPath);
            $fileSize = $fileInfo->getSize();
            $fileMtime = $fileInfo->getMTime();
            $selectStmt->execute([':file_hash' => $fileHash]);
            $existingSong = $selectStmt->fetch();
            if ($existingSong
                && !$refreshMetadata
                && !$forceMetadata
                && (int)$existingSong['is_missing'] !== 1
                && (int)$existingSong['file_size'] === (int)$fileSize
                && (int)$existingSong['file_mtime'] === (int)$fileMtime
            ) {
                $skippedCount++;
                continue;
            }
            $metadata = extractMusicMetadata($utf8FilePath);
            $normalizedFilePath = str_replace('\\', '/', $utf8FilePath);
            $relativePath = stripos($normalizedFilePath, $utf8ScanRoot) === 0
                ? substr($normalizedFilePath, strlen($utf8ScanRoot))
                : basename($normalizedFilePath);
            $folderPath = str_replace('\\', '/', dirname($utf8FilePath));
            $folderName = basename($folderPath);
            if (($metadata['tag_used'] ?? 0) == 1) $tagCount++;
            if (($metadata['filename_fallback_used'] ?? 0) == 1) $filenameCount++;
            if (!empty($metadata['has_cover'])) $coverSuccessCount++; else $coverMissingCount++;
            if (!empty($metadata['metadata_error'])) $metadataErrorCount++;
            // 인코딩 보정 여부: metadata_source가 internal이고 tag_used인 경우 인코딩 처리가 진행됨
            if (($metadata['tag_used'] ?? 0) == 1 && ($metadata['metadata_source'] ?? '') === 'internal') $encodingFixCount++;

            $fileParams = [
                ':file_path' => $utf8FilePath,
                ':file_hash' => $fileHash,
                ':folder_path' => $folderPath,
                ':folder_name' => $folderName,
                ':relative_path' => $relativePath,
                ':file_size' => $fileSize,
                ':file_mtime' => $fileMtime,
                ':extension' => $ext,
            ];

            if ($existingSong) {
                $shouldUpdate = $refreshMetadata
                    || $forceMetadata
                    || $existingSong['file_size'] != $fileSize
                    || $existingSong['file_mtime'] != $fileMtime
                    || $existingSong['is_missing'] == 1
                    || ($metadata['year'] !== null && $existingSong['year'] === null)
                    || ($metadata['cover_hash'] !== null && $existingSong['cover_hash'] === null);

                if ($shouldUpdate) {
                    $protectManual = settingBool('scan_protect_manual_edited', true);
                    $manualProtected = $protectManual && ((int)$existingSong['is_manual_edited'] === 1) && !$forceMetadata;
                    if ($manualProtected) {
                        $manualProtectedCount++;
                        $updateTechnicalStmt->execute(array_merge([
                            ':duration' => $metadata['duration'],
                            ':bitrate' => $metadata['bitrate'],
                            ':sample_rate' => $metadata['sample_rate'],
                            ':channels' => $metadata['channels'],
                            ':cover_hash' => $metadata['cover_hash'],
                            ':cover_mime' => $metadata['cover_mime'],
                            ':has_cover' => $metadata['has_cover'],
                            ':metadata_source' => $metadata['metadata_source'],
                            ':metadata_error' => $metadata['metadata_error'],
                        ], $fileParams));
                    } else {
                        if (settingBool('scan_fill_empty_only', false) && !$forceMetadata) {
                            if (!empty($existingSong['title'])) $metadata['title'] = $existingSong['title'];
                            if (!empty($existingSong['artist'])) $metadata['artist'] = $existingSong['artist'];
                            if (!empty($existingSong['album'])) $metadata['album'] = $existingSong['album'];
                            if (!empty($existingSong['genre'])) $metadata['genre'] = $existingSong['genre'];
                        }
                        $updateFullStmt->execute(array_merge(scanMetadataParams($metadata), $fileParams));
                    }
                    $updatedCount++;
                } elseif ($existingSong['is_missing'] == 1) {
                    $markActiveStmt->execute([':file_hash' => $fileHash]);
                    $updatedCount++;
                } else {
                    $skippedCount++;
                }
            } else {
                $insertStmt->execute(array_merge(scanMetadataParams($metadata), $fileParams));
                $newCount++;
            }
        } catch (Throwable $fileEx) {
            $errorCount++;
            $safeFileName = isset($winFilePath) ? basename(toUtf8Path($winFilePath)) : '알 수 없는 파일';
            $safeError = limitLogText($fileEx->getMessage(), 500);
            scanLogMessage('파일 처리 오류: ' . $safeFileName . ' / ' . $safeError, 'WARNING');
        }
    }

    $pathLikeCandidates = [];
    foreach ([$scanPath, toUtf8Path($realWinScanPath), $utf8ScanRoot] as $candidatePath) {
        $candidatePath = rtrim((string)$candidatePath, "\\/");
        if ($candidatePath === '') continue;
        $pathLikeCandidates[] = str_replace('\\', '/', $candidatePath) . '/%';
    }
    $pathLikeCandidates = array_values(array_unique($pathLikeCandidates));
    $pathClauses = [];
    $pathParams = [];
    foreach ($pathLikeCandidates as $idx => $candidatePath) {
        $key = ':scan_path_' . $idx;
        $pathClauses[] = "REPLACE(file_path, '\\\\', '/') LIKE " . $key;
        $pathParams[$key] = $candidatePath;
    }
    $activeSql = "SELECT id, file_hash, file_path, title FROM songs WHERE is_missing = 0";
    if ($pathClauses) {
        $activeSql .= ' AND (' . implode(' OR ', $pathClauses) . ')';
    }
    $activeStmt = $pdo->prepare($activeSql);
    $activeStmt->execute($pathParams);
    $activeSongs = $activeStmt->fetchAll();
    $markMissingStmt = $pdo->prepare("UPDATE songs SET is_missing = 1 WHERE id = :id");
    foreach ($activeSongs as $song) {
        if (scanPathWithinRoot($song['file_path'], $normalizedScanRoot) && !isset($foundHashes[$song['file_hash']])) {
            $markMissingStmt->execute([':id' => $song['id']]);
            $deletedCount++;
        }
    }

    $scanFinishedAt = date('Y-m-d H:i:s');
    $elapsedTime = round(microtime(true) - $startTime, 2);
    $message = buildKoreanScanSummary([
        'total_found' => $totalFound,
        'new_count' => $newCount,
        'updated_count' => $updatedCount,
        'skipped_count' => $skippedCount,
        'missing_count' => $deletedCount,
        'tag_count' => $tagCount,
        'filename_count' => $filenameCount,
        'manual_protected_count' => $manualProtectedCount,
        'cover_success_count' => $coverSuccessCount,
        'cover_missing_count' => $coverMissingCount,
        'metadata_error_count' => $metadataErrorCount,
        'encoding_fixed' => $encodingFixCount,
        'file_errors' => $errorCount,
        'elapsed' => $elapsedTime,
    ]);
    scanLogMessage($message);

    $logInsertStmt = $pdo->prepare("
        INSERT INTO scan_logs (
            scan_started_at, scan_finished_at, total_found, new_count, updated_count, deleted_count,
            skipped_count, tag_count, filename_count, manual_protected_count, cover_success_count,
            cover_missing_count, metadata_error_count, missing_count, message
        ) VALUES (
            :scan_started_at, :scan_finished_at, :total_found, :new_count, :updated_count, :deleted_count,
            :skipped_count, :tag_count, :filename_count, :manual_protected_count, :cover_success_count,
            :cover_missing_count, :metadata_error_count, :missing_count, :message
        )
    ");
    $logInsertStmt->execute([
        ':scan_started_at' => $scanStartedAt,
        ':scan_finished_at' => $scanFinishedAt,
        ':total_found' => $totalFound,
        ':new_count' => $newCount,
        ':updated_count' => $updatedCount,
        ':deleted_count' => $deletedCount,
        ':skipped_count' => $skippedCount,
        ':tag_count' => $tagCount,
        ':filename_count' => $filenameCount,
        ':manual_protected_count' => $manualProtectedCount,
        ':cover_success_count' => $coverSuccessCount,
        ':cover_missing_count' => $coverMissingCount,
        ':metadata_error_count' => $metadataErrorCount,
        ':missing_count' => $deletedCount,
        ':message' => $message,
    ]);
    finishAdminJob($adminJobId, 'completed', [
        'processed_count' => $totalFound,
        'failed_count' => $errorCount + $metadataErrorCount,
        'summary_message' => $message,
        'context' => [
            'scan_mode' => $requestedScanMode,
            'new_count' => $newCount,
            'updated_count' => $updatedCount,
            'missing_count' => $deletedCount,
            'cover_missing_count' => $coverMissingCount,
            'metadata_error_count' => $metadataErrorCount,
        ],
    ]);
} catch (Throwable $e) {
    $message = '스캔 실패: ' . maskInternalPathText(ensureUtf8String($e->getMessage()));
    scanLogMessage($message, 'ERROR');
    finishAdminJob($adminJobId, 'failed', [
        'processed_count' => $totalFound,
        'failed_count' => max(1, $errorCount + $metadataErrorCount),
        'summary_message' => $message,
        'error_type' => 'scan_error',
        'context' => [
            'scan_mode' => $requestedScanMode ?? 'settings',
        ],
    ]);
    try {
        $pdo = getDBConnection();
        $logInsertStmt = $pdo->prepare("INSERT INTO scan_logs (scan_started_at, scan_finished_at, message) VALUES (:scan_started_at, :scan_finished_at, :message)");
        $logInsertStmt->execute([
            ':scan_started_at' => $scanStartedAt,
            ':scan_finished_at' => date('Y-m-d H:i:s'),
            ':message' => $message,
        ]);
    } catch (Throwable $ignored) {
    }
    if (!$isCli) {
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        echo json_encode(['success' => false, 'message' => $message], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        exit;
    }
}

if (!$isCli) {
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    echo json_encode([
        'success' => $errorCount === 0,
        'message' => $message,
        'mode' => $requestedScanMode ?? 'settings',
        'stats' => [
            'total'            => $totalFound,
            'new'              => $newCount,
            'updated'          => $updatedCount,
            'skipped'          => $skippedCount,
            'deleted'          => $deletedCount,
            'errors'           => $errorCount,
            'tag'              => $tagCount,
            'filename'         => $filenameCount,
            'manual_protected' => $manualProtectedCount,
            'cover'            => $coverSuccessCount,
            'no_cover'         => $coverMissingCount,
            'metadata_errors'  => $metadataErrorCount,
            'encoding_fixed'   => $encodingFixCount,
        ],
    ], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
}
