<?php

declare(strict_types=1);

/**
 * Instagram-Sync (Juicer-Ersatz): holt die letzten Beiträge des öffentlichen
 * Profils, lädt die Bilder lokal nach public/assets/img/instagram/ und schreibt
 * data/instagram.json atomar. Bei JEDEM Fehler bleibt der letzte gute Cache
 * unangetastet — die Website degradiert nur, sie bricht nie.
 *
 * Aufruf:  php bin/instagram-sync.php [-v]
 * Cron:    17 6,18 * * * cd /pfad/zur/site && /usr/bin/php bin/instagram-sync.php >> storage/logs/instagram.log 2>&1
 *
 * Strategie-Kette (erster Erfolg gewinnt):
 *   1. feed/user-API (JSON, items[] — antwortet auch dann, wenn web_profile_info 400 wirft)
 *   2. web_profile_info-API (JSON, edges[])
 *   3. Profil-HTML nach eingebettetem JSON parsen
 */
// Nur CLI — oder der authentifizierte Cron-Endpoint public/cron.php, der CRON_HTTP
// setzt (der Tarif kennt keine Cronjobs, siehe CLAUDE.md: Ausnahme zu Regel 3).
if (PHP_SAPI !== 'cli' && !defined('CRON_HTTP')) {
    exit(1);
}

// require_once, weil cron.php den Bootstrap schon geladen hat: app/bootstrap.php
// definiert Konstanten und lädt helpers.php, ein zweiter Durchlauf wäre ein Fatal.
require_once dirname(__DIR__) . '/app/bootstrap.php';

$verbose = in_array('-v', $argv ?? [], true);
$username = (string) config('instagram.username', 'tsv08kulmbach');
$maxPosts = (int) config('instagram.max_posts', 9);
$imgDir = PUBLIC_PATH . '/assets/img/instagram';
$cacheFile = DATA_PATH . '/instagram.json';

// Schreibt über die zentrale log_write() (helpers.php) nach storage/logs/instagram.log;
// ERROR landet zusätzlich in error.log. Mit -v zusätzlich auf stdout.
$log = static function (string $msg, string $level = 'INFO') use ($verbose): void {
    log_write('instagram', $level, $msg);
    if ($verbose) {
        echo '[' . date('c') . "] {$level}  {$msg}\n";
    }
};

$fail = static function (string $msg) use ($log): never {
    $log("{$msg} — bestehender Cache bleibt unangetastet.", 'ERROR');
    exit(1);
};

// --- Lock gegen parallele Läufe ---
$lock = fopen(STORAGE_PATH . '/cache/instagram.lock', 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
    // Kein ERROR: ein überlappender Cron-Lauf ist kein Ausfall, der nächste Takt holt es nach.
    $log('Läuft bereits (Lock belegt) — Lauf übersprungen.', 'WARN');
    exit(1);
}

// --- HTTP-Helfer mit Browser-typischen Headern ---
$fetch = static function (string $url, array $headers = []): string|false {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_ENCODING       => '',
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
        CURLOPT_HTTPHEADER     => array_merge(['Accept-Language: de-DE,de;q=0.9'], $headers),
    ]);
    $body = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    return ($body !== false && $status === 200) ? $body : false;
};

/**
 * Posts aus der web_profile_info-JSON-Struktur ziehen.
 * @return array<int, array<string, mixed>>
 */
$extractPosts = static function (array $data) use ($maxPosts): array {
    $edges = $data['data']['user']['edge_owner_to_timeline_media']['edges']
        ?? $data['graphql']['user']['edge_owner_to_timeline_media']['edges']
        ?? [];
    $posts = [];
    foreach (array_slice($edges, 0, $maxPosts) as $edge) {
        $node = $edge['node'] ?? [];
        if (empty($node['shortcode']) || empty($node['display_url'])) {
            continue;
        }
        $caption = trim((string) ($node['edge_media_to_caption']['edges'][0]['node']['text'] ?? ''));
        $posts[] = [
            'shortcode'   => (string) $node['shortcode'],
            'caption'     => $caption,
            'taken_at'    => (int) ($node['taken_at_timestamp'] ?? 0),
            'is_video'    => (bool) ($node['is_video'] ?? false),
            'display_url' => (string) $node['display_url'],
        ];
    }
    return $posts;
};

/**
 * Posts aus der feed/user-Struktur ziehen (items[] mit image_versions2 statt edges[]).
 * @return array<int, array<string, mixed>>
 */
$extractFeedItems = static function (array $data) use ($maxPosts): array {
    $posts = [];
    foreach (array_slice($data['items'] ?? [], 0, $maxPosts) as $item) {
        // Größten Bild-Kandidaten nehmen — wird unten ohnehin auf 640x800 gecroppt.
        $url = '';
        $bestWidth = 0;
        foreach ($item['image_versions2']['candidates'] ?? [] as $candidate) {
            $width = (int) ($candidate['width'] ?? 0);
            if ($width > $bestWidth && !empty($candidate['url'])) {
                $bestWidth = $width;
                $url = (string) $candidate['url'];
            }
        }
        if (empty($item['code']) || $url === '') {
            continue;
        }
        $posts[] = [
            'shortcode'   => (string) $item['code'],
            'caption'     => trim((string) ($item['caption']['text'] ?? '')),
            'taken_at'    => (int) ($item['taken_at'] ?? 0),
            // media_type: 1 = Bild, 2 = Video/Reel, 8 = Karussell.
            'is_video'    => (int) ($item['media_type'] ?? 1) === 2 || !empty($item['video_versions']),
            'display_url' => $url,
        ];
    }
    return $posts;
};

$apiHeaders = [
    'x-ig-app-id: 936619743392459',
    'Accept: application/json',
    'Referer: https://www.instagram.com/' . $username . '/',
];

// --- Strategie 1: feed/user-API ---
$posts = [];
$body = $fetch("https://www.instagram.com/api/v1/feed/user/{$username}/username/?count=12", $apiHeaders);
if ($body !== false) {
    $data = json_decode($body, true);
    if (is_array($data)) {
        $posts = $extractFeedItems($data);
        $log('Strategie 1 (feed/user): ' . count($posts) . ' Posts');
    }
}

// --- Strategie 2: web_profile_info-API ---
if ($posts === []) {
    $body = $fetch("https://www.instagram.com/api/v1/users/web_profile_info/?username={$username}", $apiHeaders);
    if ($body !== false) {
        $data = json_decode($body, true);
        if (is_array($data)) {
            $posts = $extractPosts($data);
            $log('Strategie 2 (web_profile_info): ' . count($posts) . ' Posts');
        }
    }
}

// --- Strategie 3: Profil-HTML nach eingebettetem JSON parsen ---
if ($posts === []) {
    $html = $fetch("https://www.instagram.com/{$username}/");
    if ($html !== false) {
        // Eingebettete JSON-Blöcke (script type application/json) nach Timeline-Daten durchsuchen.
        if (preg_match_all('/<script type="application\/json"[^>]*>(.*?)<\/script>/s', $html, $m)) {
            foreach ($m[1] as $jsonBlob) {
                if (!str_contains($jsonBlob, 'edge_owner_to_timeline_media')) {
                    continue;
                }
                $data = json_decode($jsonBlob, true);
                if (!is_array($data)) {
                    continue;
                }
                // Struktur variiert — rekursiv nach edge_owner_to_timeline_media suchen.
                $stack = [$data];
                while ($stack !== [] && $posts === []) {
                    $item = array_pop($stack);
                    if (isset($item['edge_owner_to_timeline_media'])) {
                        $posts = $extractPosts(['data' => ['user' => $item]]);
                        break;
                    }
                    foreach ($item as $value) {
                        if (is_array($value)) {
                            $stack[] = $value;
                        }
                    }
                }
                if ($posts !== []) {
                    break;
                }
            }
        }
        $log('Strategie 3 (HTML-Parsing): ' . count($posts) . ' Posts');
    }
}

if ($posts === []) {
    $fail('Keine Posts ermittelbar (Instagram blockt vermutlich die Server-IP oder hat die Struktur geändert)');
}

// --- Bilder lokal laden, auf Instagram-Portrait 4:5 (640x800) bringen ---
if (!is_dir($imgDir)) {
    mkdir($imgDir, 0755, true);
}

$result = [];
foreach ($posts as $post) {
    $file = "{$imgDir}/{$post['shortcode']}.jpg";
    if (!is_file($file)) {
        $img = $fetch($post['display_url']);
        if ($img === false) {
            $log("Bild-Download fehlgeschlagen: {$post['shortcode']} — Post wird übersprungen", 'WARN');
            continue;
        }
        $src = imagecreatefromstring($img);
        if ($src === false) {
            $log("Bild unlesbar: {$post['shortcode']} — Post wird übersprungen", 'WARN');
            continue;
        }
        $w = imagesx($src);
        $h = imagesy($src);
        // Mittiger Crop auf 4:5 (Instagram-Portrait), dann auf 640x800 skalieren.
        if ($w / $h > 4 / 5) {
            $cropH = $h;
            $cropW = (int) round($h * 4 / 5);
        } else {
            $cropW = $w;
            $cropH = (int) round($w * 5 / 4);
        }
        $out = imagecreatetruecolor(640, 800);
        imagecopyresampled($out, $src, 0, 0, (int) (($w - $cropW) / 2), (int) (($h - $cropH) / 2), 640, 800, $cropW, $cropH);
        imagejpeg($out, $file, 80);
    }

    // Alt-Text aus der Caption: erste Zeile, gekürzt.
    $alt = trim(strtok($post['caption'], "\n") ?: '');
    $alt = $alt !== '' ? mb_substr($alt, 0, 120) : 'Instagram-Beitrag des TSV 08 Kulmbach';

    $result[] = [
        'shortcode' => $post['shortcode'],
        'caption'   => $post['caption'],
        'alt'       => $alt,
        'taken_at'  => $post['taken_at'],
        'is_video'  => $post['is_video'],
        'image'     => "img/instagram/{$post['shortcode']}.jpg",
        'url'       => "https://www.instagram.com/p/{$post['shortcode']}/",
    ];
}

if ($result === []) {
    $fail('Posts gefunden, aber kein einziges Bild ladbar');
}

// --- Nicht mehr aktuelle Bilder aufräumen ---
$keep = array_map(static fn (array $p): string => "{$p['shortcode']}.jpg", $result);
foreach (glob("{$imgDir}/*.jpg") ?: [] as $existing) {
    if (!in_array(basename($existing), $keep, true)) {
        unlink($existing);
    }
}

// --- Cache atomar schreiben (tmp + rename) ---
$payload = json_encode([
    'fetched_at' => time(),
    'username'   => $username,
    'posts'      => $result,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

$tmp = $cacheFile . '.tmp';
file_put_contents($tmp, $payload);
rename($tmp, $cacheFile);

$log('OK: ' . count($result) . ' Posts gecached, Bilder lokal in assets/img/instagram/');
