<?php

namespace App\Console\Commands;

use App\Notification\TelegramBot;
use App\Util;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

/**
 * 检测配置的 iOS App Store 包是否被下架，若下架则通过 Telegram 通知。
 *
 * 规则：
 * 1. 优先使用 Apple lookup 接口检测：
 *    https://itunes.apple.com/lookup?id={appId}&country={country}&entity=software
 *
 * 2. 状态分三类：
 *    - online   : 明确在线
 *    - delisted : 明确下架/该区不可用
 *    - unknown  : 暂时无法判断（429、403、5xx、超时、网络错误等）
 *
 * 3. 连续确认：
 *    - delisted 连续 N 次才发下架告警
 *    - unknown  连续 N 次才发异常告警
 *
 * 4. online 会清除之前的连续计数
 */
class CheckIosAppStore extends Command
{
    protected $signature = 'ios:check-app-store';

    protected $description = '检测 iOS App Store 包是否被下架，下架时 Telegram 通知';

    /** 要检测的 iOS 包列表：name => url */
    protected array $apps = [
        'Slots Magnet Revolution 206' => 'https://apps.apple.com/us/app/slots-magnet-revolution/id6759797102',
        'Viking Mysteric Slots 207'   => 'https://apps.apple.com/us/app/viking-mysteric-slots/id6759851510',
        'Slots Chemistry Pirate 208'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id6759852588',
//        'Mini Plinko Slot 227'  => 'https://apps.apple.com/us/app/mini-plinko-slot/id6769709635',
//        'ios test'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id67598525',
        // 'ceshi' => 'https://apps.apple.com/us/app/pixiepineslotsgrid/id6758513278',
    ];

    /**
     * 连续多少次确认“下架”才发告警
     */
    protected int $delistedConfirmTimes = 3;

    /**
     * 连续多少次确认“异常”才发告警
     */
    protected int $unknownConfirmTimes = 3;

    /**
     * Redis 计数过期时间（秒）
     * 比如每 10 分钟跑一次，3600 表示 1 小时窗口内累计
     */
    protected int $counterExpireSeconds = 3600;

    /**
     * 每次请求最大重试次数
     */
    protected int $httpMaxAttempts = 3;

    /**
     * 请求间随机抖动范围（微秒）
     */
    protected int $sleepMinMicroseconds = 200000; // 0.2 秒
    protected int $sleepMaxMicroseconds = 500000; // 0.5 秒

    public function handle()
    {
        $env = env('APP_ENV', 'local');

        $results = [];
        $delistedNotified = [];
        $unknownNotified = [];

        foreach ($this->apps as $name => $url) {
            $result = $this->checkSingleApp($name, $url);
            $results[] = $result;

            Util::WriteLog(
                'ios_app_store_check',
                sprintf(
                    '[%s] %s | %s | %s',
                    strtoupper($result['status']),
                    $name,
                    $url,
                    $result['reason']
                )
            );

            if ($result['status'] === 'online') {
                $this->clearCounters($result['app_id']);
            } elseif ($result['status'] === 'delisted') {
                $count = $this->increaseCounter($this->getDelistedCounterKey($result['app_id']));
                $this->clearCounter($this->getUnknownCounterKey($result['app_id']));

                if ($count >= $this->delistedConfirmTimes) {
                    if (!$this->hasAlreadyNotified('delisted', $result['app_id'])) {
                        $delistedNotified[] = [
                            'name'   => $name,
                            'url'    => $url,
                            'reason' => $result['reason'],
                            'count'  => $count,
                        ];
                        $this->markNotified('delisted', $result['app_id']);
                    }
                }
            } elseif ($result['status'] === 'unknown') {
                $count = $this->increaseCounter($this->getUnknownCounterKey($result['app_id']));
                $this->clearCounter($this->getDelistedCounterKey($result['app_id']));

                if ($count >= $this->unknownConfirmTimes) {
                    if (!$this->hasAlreadyNotified('unknown', $result['app_id'])) {
                        $unknownNotified[] = [
                            'name'   => $name,
                            'url'    => $url,
                            'reason' => $result['reason'],
                            'count'  => $count,
                        ];
                        $this->markNotified('unknown', $result['app_id']);
                    }
                }
            }

            // 请求间抖动，降低被 Apple 限流概率
            usleep(random_int($this->sleepMinMicroseconds, $this->sleepMaxMicroseconds));
        }

        if (!empty($delistedNotified)) {
            $this->notifyDelisted($env, $delistedNotified);
            $this->warn('已发送下架告警: ' . count($delistedNotified) . ' 个');
        }

        if (!empty($unknownNotified)) {
            $this->notifyUnknown($env, $unknownNotified);
            $this->warn('已发送检测异常告警: ' . count($unknownNotified) . ' 个');
        }

        foreach ($results as $item) {
            $this->line(sprintf(
                '[%s] %s - %s',
                strtoupper($item['status']),
                $item['name'],
                $item['reason']
            ));
        }

        if (empty($delistedNotified) && empty($unknownNotified)) {
            $this->info('本次检测完成，无需告警。');
        }

        return empty($delistedNotified) ? 0 : 1;
    }

    /**
     * 检测单个 App 状态
     *
     * 返回：
     * [
     *   'name'   => string,
     *   'url'    => string,
     *   'app_id' => string,
     *   'status' => 'online'|'delisted'|'unknown',
     *   'reason' => string,
     * ]
     */
    protected function checkSingleApp(string $name, string $appStoreUrl): array
    {
        $parsed = $this->parseAppStoreUrl($appStoreUrl);

        if (!$parsed) {
            return [
                'name'   => $name,
                'url'    => $appStoreUrl,
                'app_id' => 'unknown',
                'status' => 'unknown',
                'reason' => '无法解析 App Store URL 中的国家或 App ID',
            ];
        }

        $country = $parsed['country'];
        $appId = $parsed['app_id'];

        $lookupUrl = sprintf(
            'https://itunes.apple.com/lookup?id=%s&country=%s&entity=software',
            $appId,
            $country
        );

        $resp = $this->httpGetWithRetry($lookupUrl, $this->httpMaxAttempts);

        if (!empty($resp['error'])) {
            return [
                'name'   => $name,
                'url'    => $appStoreUrl,
                'app_id' => $appId,
                'status' => 'unknown',
                'reason' => 'Lookup 请求失败: ' . $resp['error'],
            ];
        }

        $code = (int)($resp['code'] ?? 0);
        $body = (string)($resp['body'] ?? '');

        if ($code === 200) {
            $json = json_decode($body, true);

            if (!is_array($json)) {
                return [
                    'name'   => $name,
                    'url'    => $appStoreUrl,
                    'app_id' => $appId,
                    'status' => 'unknown',
                    'reason' => 'Lookup 返回非有效 JSON',
                ];
            }

            $resultCount = (int)($json['resultCount'] ?? 0);
            $results = $json['results'] ?? [];

            if ($resultCount <= 0 || empty($results)) {
                return [
                    'name'   => $name,
                    'url'    => $appStoreUrl,
                    'app_id' => $appId,
                    'status' => 'delisted',
                    'reason' => "Lookup 无结果(resultCount=0, country={$country}, appId={$appId})",
                ];
            }

            $app = $results[0];
            $trackId = (string)($app['trackId'] ?? '');
            $trackViewUrl = (string)($app['trackViewUrl'] ?? '');

            if ($trackId !== (string)$appId) {
                return [
                    'name'   => $name,
                    'url'    => $appStoreUrl,
                    'app_id' => $appId,
                    'status' => 'unknown',
                    'reason' => 'Lookup 返回结果异常：trackId 不匹配',
                ];
            }

            if ($trackViewUrl !== '' && stripos($trackViewUrl, "/{$country}/") === false) {
                return [
                    'name'   => $name,
                    'url'    => $appStoreUrl,
                    'app_id' => $appId,
                    'status' => 'online',
                    'reason' => 'Lookup 返回应用信息（国家链接与配置不一致，但应用存在）',
                ];
            }

            // 成功在线时，清掉已通知标记，这样未来再次异常还能重新通知
            $this->clearNotifiedFlags($appId);

            return [
                'name'   => $name,
                'url'    => $appStoreUrl,
                'app_id' => $appId,
                'status' => 'online',
                'reason' => 'Lookup 正常返回应用信息',
            ];
        }

        // 这些不能直接判定为下架
        if (in_array($code, [403, 404, 429, 500, 502, 503, 504], true)) {
            return [
                'name'   => $name,
                'url'    => $appStoreUrl,
                'app_id' => $appId,
                'status' => 'unknown',
                'reason' => "Lookup HTTP {$code}",
            ];
        }

        return [
            'name'   => $name,
            'url'    => $appStoreUrl,
            'app_id' => $appId,
            'status' => 'unknown',
            'reason' => "Lookup HTTP {$code}",
        ];
    }

    /**
     * 从 App Store 链接中提取国家和 app id
     *
     * 示例：
     * https://apps.apple.com/us/app/slots-magnet-revolution/id6759797102
     */
    protected function parseAppStoreUrl(string $url): ?array
    {
        $pattern = '#https?://apps\.apple\.com/([a-z]{2})/app/.*/id(\d+)#i';

        if (!preg_match($pattern, $url, $matches)) {
            return null;
        }

        return [
            'country' => strtolower($matches[1]),
            'app_id'  => $matches[2],
        ];
    }

    /**
     * GET 请求，带简单重试和退避
     *
     * 返回：
     * [
     *   'code'  => int,
     *   'body'  => string,
     *   'error' => string,
     * ]
     */
    protected function httpGetWithRetry(string $url, int $maxAttempts = 3): array
    {
        $attempt = 0;
        $last = [
            'code'  => 0,
            'body'  => '',
            'error' => '',
        ];

        while ($attempt < $maxAttempts) {
            $attempt++;

            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_TIMEOUT => 20,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_HTTPHEADER => [
                    'Accept: application/json',
                    'Accept-Language: en-US,en;q=0.9',
                    'Cache-Control: no-cache',
                ],
                CURLOPT_USERAGENT => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
            ]);

            $body = curl_exec($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            curl_close($ch);

            $last = [
                'code'  => $code,
                'body'  => is_string($body) ? $body : '',
                'error' => $error ?: '',
            ];

            if ($last['error'] === '' && $code === 200) {
                return $last;
            }

            $shouldRetry = $last['error'] !== '' || in_array($code, [429, 500, 502, 503, 504], true);

            if (!$shouldRetry || $attempt >= $maxAttempts) {
                return $last;
            }

            if ($attempt == 1) {
                $sleepSeconds = 2;
            } elseif ($attempt == 2) {
                $sleepSeconds = 5;
            } else {
                $sleepSeconds = 8;
            }

            sleep($sleepSeconds);
        }

        return $last;
    }

    protected function getDelistedCounterKey(string $appId): string
    {
        return "ios:appstore:delisted:{$appId}";
    }

    protected function getUnknownCounterKey(string $appId): string
    {
        return "ios:appstore:unknown:{$appId}";
    }

    protected function getDelistedNotifiedKey(string $appId): string
    {
        return "ios:appstore:notified:delisted:{$appId}";
    }

    protected function getUnknownNotifiedKey(string $appId): string
    {
        return "ios:appstore:notified:unknown:{$appId}";
    }

    protected function increaseCounter(string $key): int
    {
        $count = (int) Redis::incr($key);
        Redis::expire($key, $this->counterExpireSeconds);
        return $count;
    }

    protected function clearCounters(string $appId): void
    {
        $this->clearCounter($this->getDelistedCounterKey($appId));
        $this->clearCounter($this->getUnknownCounterKey($appId));
    }

    protected function clearCounter(string $key): void
    {
        try {
            Redis::del($key);
        } catch (\Throwable $e) {
            Util::WriteLog('ios_app_store_check', 'Redis del error: ' . $e->getMessage());
        }
    }

    /**
     * 防止达到阈值后每次 cron 都重复发同类告警
     */
    protected function hasAlreadyNotified(string $type, string $appId): bool
    {
        try {
            $key = $type === 'delisted'
                ? $this->getDelistedNotifiedKey($appId)
                : $this->getUnknownNotifiedKey($appId);

            return (bool) Redis::exists($key);
        } catch (\Throwable $e) {
            Util::WriteLog('ios_app_store_check', 'Redis exists error: ' . $e->getMessage());
            return false;
        }
    }

    protected function markNotified(string $type, string $appId): void
    {
        try {
            $key = $type === 'delisted'
                ? $this->getDelistedNotifiedKey($appId)
                : $this->getUnknownNotifiedKey($appId);

            // 通知标记也设置一个过期，防止永远不恢复
            Redis::setex($key, $this->counterExpireSeconds, 1);
        } catch (\Throwable $e) {
            Util::WriteLog('ios_app_store_check', 'Redis setex error: ' . $e->getMessage());
        }
    }

    protected function clearNotifiedFlags(string $appId): void
    {
        try {
            Redis::del(
                $this->getDelistedNotifiedKey($appId),
                $this->getUnknownNotifiedKey($appId)
            );
        } catch (\Throwable $e) {
            Util::WriteLog('ios_app_store_check', 'Redis clear notified flags error: ' . $e->getMessage());
        }
    }

    protected function notifyDelisted(string $env, array $items): void
    {
        $lines = ["[{$env}] iOS App Store 下架检测告警"];

        foreach ($items as $item) {
            $lines[] = "• {$item['name']}";
            $lines[] = "  原因: {$item['reason']}";
            $lines[] = "  连续次数: {$item['count']}";
            $lines[] = "  链接: {$item['url']}";
        }

        $this->sendTelegramMessage(implode("\n", $lines));
    }

    protected function notifyUnknown(string $env, array $items): void
    {
        $lines = ["[{$env}] iOS App Store 检测异常告警"];

        foreach ($items as $item) {
            $lines[] = "• {$item['name']}";
            $lines[] = "  原因: {$item['reason']}";
            $lines[] = "  连续次数: {$item['count']}";
            $lines[] = "  链接: {$item['url']}";
        }

        $this->sendTelegramMessage(implode("\n", $lines));
    }

    protected function sendTelegramMessage(string $message): void
    {
        try {
//            TelegramBot::getDefault()->sendMsg($message);
            TelegramBot::getDefault()->sendMsgAndImportant($message);
        } catch (\Throwable $e) {
            Util::WriteLog('ios_app_store_check', 'Telegram send error: ' . $e->getMessage());
            $this->error('Telegram 发送失败: ' . $e->getMessage());
        }
    }
}