<?php

declare(strict_types=1);



class FeedImportProcessor
{
    private const CRON_INTERVAL  = 30;
    private const MAX_TICK_SECONDS = 9;
    private const EMBED_PER_TICK = 8;    
    private const DOWNLOAD_PER_TICK = 1;  
    private const STALE_SECONDS = 1800;  
    private const MAX_ATTEMPTS = 3;
    private const UA = 'Mozilla/5.0 (compatible; TubePressFeedBot/1.0; +https://tubepress.io)';
    private const MAX_VIDEO_BYTES = 5368709120;  

    public static function registerCron(): void
    {
        CronManager::register('feed_import', self::CRON_INTERVAL, [self::class, 'tick']);
    }

     
    public static function tick(): void
    {
        self::recoverStalled();
        self::fetchDueSources();
        self::processPending();
    }

     

    public static function fetchSource(int $sourceId): array
    {
        $src = Database::fetchOne('SELECT * FROM feed_sources WHERE id = ?', [$sourceId]);
        if (!$src) {
            return ['ok' => false, 'error' => 'Source not found', 'fetched' => 0, 'new' => 0, 'dup' => 0];
        }

        $parsed = FeedParser::parsePaged(
            (string) $src['url'],
            (string) $src['type'],
            (string) $src['page_param'],
            (int) $src['max_pages']
        );
        $rows      = $parsed['rows'];
        $canonical = $parsed['canonical'];
        $mapping   = $canonical
            ? FeedParser::defaultMapping()
            : (json_decode((string) ($src['mapping'] ?? ''), true) ?: []);

        $new = 0;
        $dup = 0;
        foreach ($rows as $row) {
            $payload = self::normalizePayload($row, $mapping, $canonical);
            $guid = self::itemGuid($payload);
            if ($guid === '' || $payload['title'] === '') {
                continue;
            }
            $payload['guid'] = $guid;
            $inserted = Database::query(
                "INSERT IGNORE INTO feed_items (source_id, guid, title, payload, status)
                 VALUES (?, ?, ?, ?, 'pending')",
                [$sourceId, $guid, mb_substr($payload['title'], 0, 500), json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]
            )->rowCount();
            $inserted > 0 ? $new++ : $dup++;
        }

        $errorStr = !empty($parsed['errors']) ? implode('; ', $parsed['errors']) : '';
        $status   = ($errorStr !== '' && empty($rows)) ? 'error' : 'ok';
        $message  = $errorStr !== ''
            ? $errorStr
            : (count($rows) . ' fetched · ' . $new . ' new · ' . $dup . ' already seen');
        Database::query(
            "UPDATE feed_sources
             SET last_run_at = NOW(), last_status = ?, last_message = ?, stat_fetched = stat_fetched + ?
             WHERE id = ?",
            [$status, mb_substr($message, 0, 500), count($rows), $sourceId]
        );

        return ['ok' => $status === 'ok', 'error' => $errorStr, 'fetched' => count($rows), 'new' => $new, 'dup' => $dup];
    }

     
    public static function preview(string $url, string $type, string $pageParam, int $maxPages, int $limit = 24): array
    {
        $res = FeedParser::fetchUrl($url, 25);
        if (!$res['ok']) {
            return ['ok' => false, 'error' => $res['error'], 'type' => $type, 'items' => [], 'columns' => []];
        }
        $detected = ($type === 'auto' || $type === '') ? FeedParser::detectType($res['body'], $res['contentType']) : $type;
        if ($detected === 'unknown') {
            return ['ok' => false, 'error' => 'Could not recognize the feed format (not MRSS/RSS/CSV/JSON).', 'type' => $detected, 'items' => [], 'columns' => []];
        }
        $parsed = FeedParser::parse($res['body'], $detected);
        $rows = array_slice($parsed['rows'], 0, $limit);

        if ($parsed['canonical']) {
            $items = array_map(fn($r) => [
                'title'     => $r['title'],
                'thumbnail' => $r['thumbnail'],
                'duration'  => self::parseDuration($r['duration']),
                'video_url' => $r['video_url'],
                'embed_url' => $r['embed_url'],
                'tags'      => $r['tags'],
                'categories'=> $r['categories'],
            ], $rows);
        } else {
            $items = $rows;  
        }

        return [
            'ok'        => true,
            'error'     => '',
            'type'      => $detected,
            'canonical' => $parsed['canonical'],
            'count'     => count($parsed['rows']),
            'items'     => $items,
            'columns'   => $parsed['columns'],
        ];
    }

    private static function fetchDueSources(): void
    {
        $due = Database::fetchAll(
            "SELECT id FROM feed_sources
             WHERE enabled = 1 AND schedule_interval > 0
               AND (last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL schedule_interval SECOND))
             ORDER BY last_run_at IS NULL DESC, last_run_at ASC
             LIMIT 1"
        );
        foreach ($due as $row) {
            try {
                self::fetchSource((int) $row['id']);
            } catch (\Throwable $e) {
                Database::query(
                    "UPDATE feed_sources SET last_status='error', last_message=?, last_run_at=NOW() WHERE id=?",
                    [mb_substr('Fetch error: ' . $e->getMessage(), 0, 500), (int) $row['id']]
                );
            }
        }
    }

     

    public static function processPending(?int $onlySourceId = null): int
    {
        $start = time();
        $done = 0;
        $downloads = 0;

        while ((time() - $start) < self::MAX_TICK_SECONDS) {
            $where = "status = 'pending'" . ($onlySourceId ? " AND source_id = " . (int) $onlySourceId : '');
            $item = Database::fetchOne("SELECT * FROM feed_items WHERE $where ORDER BY id ASC LIMIT 1");
            if (!$item) {
                break;
            }
             
            $claimed = Database::query(
                "UPDATE feed_items SET status='importing', attempts = attempts + 1 WHERE id = ? AND status = 'pending'",
                [(int) $item['id']]
            )->rowCount();
            if (!$claimed) {
                continue;
            }

            $src = Database::fetchOne('SELECT * FROM feed_sources WHERE id = ?', [(int) $item['source_id']]);
            if (!$src) {
                Database::query("UPDATE feed_items SET status='failed', error='Source removed' WHERE id=?", [(int) $item['id']]);
                continue;
            }

            $isDownload = ($src['mode'] === 'download');
            if ($isDownload && $downloads >= self::DOWNLOAD_PER_TICK) {
                 
                Database::query("UPDATE feed_items SET status='pending', attempts = attempts - 1 WHERE id=?", [(int) $item['id']]);
                break;
            }

            $payload = json_decode((string) $item['payload'], true) ?: [];
            $isAlbum = trim((string) ($payload['media_type'] ?? '')) === 'album'
                || (trim((string) ($payload['images'] ?? '')) !== '' && trim((string) ($payload['video_url'] ?? '')) === '' && trim((string) ($payload['embed_url'] ?? '')) === '');
            try {
                $res = $isAlbum
                    ? self::createAlbumFromFeed($src, $payload)
                    : ($isDownload
                        ? self::createDownloadVideo($src, $payload)
                        : self::createEmbedVideo($src, $payload));
            } catch (\Throwable $e) {
                $res = ['status' => 'failed', 'error' => $e->getMessage()];
            }

            $status  = $res['status'] ?? 'failed';
            $videoId = $res['video_id'] ?? null;
            $error   = $res['error'] ?? '';

             
            if ($status === 'failed' && (int) $item['attempts'] < self::MAX_ATTEMPTS) {
                Database::query("UPDATE feed_items SET status='pending', error=? WHERE id=?", [mb_substr($error, 0, 500), (int) $item['id']]);
            } else {
                Database::query(
                    "UPDATE feed_items SET status=?, video_id=?, error=? WHERE id=?",
                    [$status, $videoId, mb_substr($error, 0, 500), (int) $item['id']]
                );
                $col = $status === 'imported' ? 'stat_imported' : ($status === 'skipped' ? 'stat_skipped' : 'stat_failed');
                Database::query("UPDATE feed_sources SET $col = $col + 1 WHERE id = ?", [(int) $item['source_id']]);
            }

            $done++;
            if ($isDownload) {
                $downloads++;
            }
        }
        return $done;
    }

    private static function recoverStalled(): void
    {
        Database::query(
            "UPDATE feed_items SET status='pending'
             WHERE status='importing' AND updated_at < DATE_SUB(NOW(), INTERVAL ? SECOND)",
            [self::STALE_SECONDS]
        );
    }

     
    private static function createAlbumFromFeed(array $src, array $payload): array
    {
        if (trim((string) ($payload['title'] ?? '')) === '') {
            return ['status' => 'failed', 'error' => 'Missing title'];
        }
        if (trim((string) ($payload['images'] ?? '')) === '' && trim((string) ($payload['thumbnail'] ?? '')) === '') {
            return ['status' => 'failed', 'error' => 'Gallery has no images'];
        }
         
        $mapping = [
            'title'        => 'title',
            'description'  => 'description',
            'images'       => 'images',
            'thumbnail'    => 'thumbnail',
            'media_type'   => 'media_type',
            'published_at' => 'published_at',
            'views'        => 'views',
            'video_id'     => 'guid',
            'categories'   => 'categories',
            'tags'         => 'tags',
            'performers'   => 'performers',
            'channels'     => 'channels',
        ];
        $opts = json_decode((string) ($src['options'] ?? ''), true) ?: [];
        $options = [
            'duplicateMode' => (($opts['dedup'] ?? 'skip') === 'update') ? 'update' : 'skip',
            'videoType'     => self::validType($src['video_type'] ?? 'straight'),
        ];
        $res = AdminImportController::importRow($payload, $mapping, $options, null);
        $albumId = $res['videoId'] ?? null;
        $st = $res['status'] ?? 'failed';
        if ($albumId) {
            $status = ((int) ($src['auto_publish'] ?? 0) === 1) ? 'published' : 'draft';
            Database::query('UPDATE albums SET status = ? WHERE id = ?', [$status, (int) $albumId]);
        }
        $mapped = ($st === 'imported' || $st === 'updated') ? 'imported' : ($st === 'skipped' ? 'skipped' : 'failed');
        return ['status' => $mapped, 'video_id' => $albumId ? (int) $albumId : null];
    }

     
    private static function createEmbedVideo(array $src, array $payload): array
    {
        $embedSource = trim((string) ($payload['embed_url'] ?? '')) ?: trim((string) ($payload['video_url'] ?? ''));
        if (trim((string) ($payload['title'] ?? '')) === '' || $embedSource === '') {
            return ['status' => 'failed', 'error' => 'Missing title or embed/player URL'];
        }
        $row = $payload;
        $row['_embed'] = $embedSource;

        $mapping = [
            'title'        => 'title',
            'description'  => 'description',
            'embed_code'   => '_embed',
            'thumbnail'    => 'thumbnail',
            'duration'     => 'duration',
            'published_at' => 'published_at',
            'views'        => 'views',
            'video_id'     => 'guid',
            'categories'   => 'categories',
            'tags'         => 'tags',
            'performers'   => 'performers',
            'channels'      => 'channels',
        ];
        $opts = json_decode((string) ($src['options'] ?? ''), true) ?: [];
        $options = [
            'duplicateMode' => (($opts['dedup'] ?? 'skip') === 'update') ? 'update' : 'skip',
            'cacheThumbs'   => false,  
        ];

        $res = AdminImportController::importRow($row, $mapping, $options, null);
        $videoId = $res['videoId'] ?? null;
        $st = $res['status'] ?? 'failed';

        if ($videoId) {
            $vtype  = self::validType($src['video_type'] ?? 'straight');
            $status = ((int) $src['auto_publish'] === 1) ? 'published' : 'draft';
            Database::query('UPDATE videos SET video_type = ?, status = ? WHERE id = ?', [$vtype, $status, (int) $videoId]);
        }
        $mapped = ($st === 'imported' || $st === 'updated') ? 'imported' : ($st === 'skipped' ? 'skipped' : 'failed');
        return ['status' => $mapped, 'video_id' => $videoId ? (int) $videoId : null];
    }

     
    private static function createDownloadVideo(array $src, array $payload): array
    {
        $title    = trim((string) ($payload['title'] ?? ''));
        $videoUrl = trim((string) ($payload['video_url'] ?? '')) ?: trim((string) ($payload['embed_url'] ?? ''));
        if ($title === '' || !preg_match('#^https?://#i', $videoUrl)) {
            return ['status' => 'failed', 'error' => 'Missing title or a direct (downloadable) video URL'];
        }
        $guid = trim((string) ($payload['guid'] ?? ''));
        if ($guid === '') {
            $guid = 'feed_' . md5($title . '|' . $videoUrl);
        }
         
        $existing = Database::fetchOne('SELECT id FROM videos WHERE external_id = ?', [$guid]);
        if ($existing) {
            return ['status' => 'skipped', 'video_id' => (int) $existing['id']];
        }

        $sidRaw = MediaStorage::activeDefaultId();
        $sid    = $sidRaw > 0 ? $sidRaw : null;
        $folder = VideoAsset::createFolder();
        $dir    = VideoAsset::dir($folder);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

         
        $part  = $dir . '/video.mp4.part';
        $final = $dir . '/video.mp4';
        if (!self::streamDownload($videoUrl, $part, self::MAX_VIDEO_BYTES)) {
            @unlink($part);
            self::cleanupDir($dir);
            return ['status' => 'failed', 'error' => 'Video download failed (' . mb_substr($videoUrl, 0, 120) . ')'];
        }
        @rename($part, $final);

         
        $thumbUrl = trim((string) ($payload['thumbnail'] ?? ''));
        $haveThumb = false;
        if (preg_match('#^https?://#i', $thumbUrl)) {
            if (self::streamDownload($thumbUrl, $dir . '/thumb.jpg', 64 * 1024 * 1024) && filesize($dir . '/thumb.jpg') > 256) {
                $haveThumb = true;
            } else {
                @unlink($dir . '/thumb.jpg');
            }
        }

         
        if ($sid && MediaStorage::isRemote($sid)) {
            if (!MediaStorage::put($final, 'uploads/videos/' . $folder . '/video.mp4', $sid)) {
                self::cleanupDir($dir);
                return ['status' => 'failed', 'error' => 'Upload to storage server failed'];
            }
            if ($haveThumb) {
                MediaStorage::put($dir . '/thumb.jpg', 'uploads/videos/' . $folder . '/thumb.jpg', $sid);
            }
        }

         
        $pub    = AdminImportController::parseDate(trim((string) ($payload['published_at'] ?? ''))) ?? date('Y-m-d H:i:s');
        $status = ((int) $src['auto_publish'] === 1) ? 'published' : 'draft';
        $data = [
            'title'        => Sanitizer::decodeEntities($title) ?? $title,
            'description'  => Sanitizer::decodeEntities(trim((string) ($payload['description'] ?? ''))) ?? '',
            'embed_code'   => '',
            'thumbnail'    => null,
            'video_folder' => $folder,
            'storage_id'   => $sid,
            'duration'     => self::parseDuration($payload['duration'] ?? ''),
            'views'        => max(0, (int) ($payload['views'] ?? 0)),
            'status'       => $status,
            'video_type'   => self::validType($src['video_type'] ?? 'straight'),
            'external_id'  => $guid,
            'published_at' => $pub,
            'user_id'      => null,
            'slug'         => Slug::unique('videos', $title),
        ];
        try {
            $videoId = Video::create($data);
        } catch (\PDOException $e) {
            self::cleanupDir($dir);
            if ($e->getCode() === '23000') {
                $ex = Database::fetchOne('SELECT id FROM videos WHERE external_id = ?', [$guid]);
                return ['status' => 'skipped', 'video_id' => $ex ? (int) $ex['id'] : null];
            }
            throw $e;
        }

         
        self::syncRelations((int) $videoId, $payload, self::validType($src['video_type'] ?? 'straight'));
        $queued = 0;
        if (TranscodeManager::isEnabled()) {
            $queued = (int) TranscodeManager::queue((int) $videoId);
        }
         
         
         
         
        if ($sid && MediaStorage::isRemote($sid)) {
            if ($queued > 0) {
                @unlink($dir . '/thumb.jpg');  
            } else {
                VideoAsset::purgeLocalFolder($folder);
            }
        }
        return ['status' => 'imported', 'video_id' => (int) $videoId];
    }

    private static function syncRelations(int $videoId, array $payload, string $videoType): void
    {
        $finders = [
            'categories' => fn($n) => AdminImportController::findOrCreateCategory($n),
            'tags'       => fn($n) => Tag::findOrCreate($n, $videoType),
            'performers' => fn($n) => AdminImportController::findOrCreatePerformer($n),
            'channels'    => fn($n) => AdminImportController::findOrCreateChannel($n),
        ];
        $syncers = [
            'categories' => [Video::class, 'syncCategories'],
            'tags'       => [Video::class, 'syncTags'],
            'performers' => [Video::class, 'syncPerformers'],
            'channels'    => [Video::class, 'syncChannels'],
        ];
        foreach ($finders as $field => $find) {
            $raw = trim((string) ($payload[$field] ?? ''));
            if ($raw === '') {
                continue;
            }
            $ids = [];
            foreach (preg_split('/[,;|]+/', $raw) as $name) {
                $name = trim($name);
                if ($name !== '') {
                    $ids[] = $find($name);
                }
            }
            if ($ids) {
                $syncers[$field]($videoId, $ids);
            }
        }
    }

    private static function streamDownload(string $url, string $dest, int $maxBytes): bool
    {
         
        if (!FeedParser::isSafePublicUrl($url)) {
            return false;
        }
        $fp = @fopen($dest, 'w');
        if (!$fp) {
            return false;
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_FILE           => $fp,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 5,
            CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_TIMEOUT        => 1800,
            CURLOPT_CONNECTTIMEOUT => 25,
            CURLOPT_USERAGENT      => self::UA,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_FAILONERROR    => true,
            CURLOPT_NOPROGRESS     => false,
            CURLOPT_PROGRESSFUNCTION => function ($ch, $dlTotal, $dlNow) use ($maxBytes) {
                return ($dlNow > $maxBytes) ? 1 : 0;
            },
        ]);
        $ok   = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        fclose($fp);
        if (!$ok || $code >= 400 || !file_exists($dest) || filesize($dest) < 1024) {
            @unlink($dest);
            return false;
        }
        return true;
    }

    private static function cleanupDir(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        foreach (glob($dir . '/*') ?: [] as $f) {
            @unlink($f);
        }
        @rmdir($dir);
    }

     
    private static function normalizePayload(array $row, array $mapping, bool $canonical): array
    {
        $out = [];
        foreach (FeedParser::CANONICAL_KEYS as $k) {
            if ($canonical) {
                $out[$k] = (string) ($row[$k] ?? '');
            } else {
                $col = $mapping[$k] ?? '';
                $out[$k] = ($col !== '' && isset($row[$col])) ? (string) $row[$col] : '';
            }
        }
         
        if (!$canonical && $out['embed_url'] === '' && $out['video_url'] !== '') {
            $out['embed_url'] = $out['video_url'];
        }
        $out['title'] = trim($out['title']);
        return $out;
    }

    private static function itemGuid(array $payload): string
    {
        $g = trim((string) ($payload['guid'] ?? ''));
        if ($g !== '') {
            return mb_substr($g, 0, 255);
        }
        $basis = ($payload['video_url'] ?? '') . '|' . ($payload['embed_url'] ?? '') . '|' . ($payload['title'] ?? '');
        return $basis === '||' ? '' : md5($basis);
    }

    private static function parseDuration($raw): int
    {
        $raw = trim((string) $raw);
        if ($raw === '') {
            return 0;
        }
        if (preg_match('/^(\d+):(\d{2}):(\d{2})$/', $raw, $m)) {
            return (int) $m[1] * 3600 + (int) $m[2] * 60 + (int) $m[3];
        }
        if (preg_match('/^(\d+):(\d{2})$/', $raw, $m)) {
            return (int) $m[1] * 60 + (int) $m[2];
        }
        return max(0, (int) $raw);
    }

    private static function validType(string $type): string
    {
        $type = strtolower(trim($type));
        $valid = defined('Video::VALID_TYPES') ? Video::VALID_TYPES : ['straight', 'gay', 'hentai'];
        return in_array($type, $valid, true) ? $type : ($valid[0] ?? 'straight');
    }
}
