<?php

declare(strict_types=1);

require_once __DIR__ . '/product_db.php';

/**
 * Minimal DMM Affiliate API v3 client for mixhost deployment.
 */
final class DmmApiClient
{
    private string $apiId;
    private string $affiliateId;
    private string $cacheDir;
    private ?int $floorIdCache = null;
    private ?ProductDb $productDb = null;

    public function __construct(string $apiId, string $affiliateId, string $cacheDir)
    {
        $this->apiId = $apiId;
        $this->affiliateId = $affiliateId;
        $this->cacheDir = rtrim($cacheDir, '/');
        $this->initProductDb();
    }

    /**
     * @return array{item:?array,error:?string,raw?:array}
     */
    public function fetchItemByCid(string $cid, int $ttlSeconds = 3600): array
    {
        $cid = trim($cid);
        if ($cid == '') {
            return ['item' => null, 'error' => 'cid is required'];
        }

        $list = $this->fetchItemList('', 1, 1, ['cid' => $cid], $ttlSeconds);
        $item = null;
        if (isset($list['items'][0]) && is_array($list['items'][0])) {
            $item = $list['items'][0];
        }
        return ['item' => $item, 'error' => $list['error'], 'raw' => $list['raw'] ?? null];
    }

    /**
     * @return array{items:array<int,array>,error:?string,total_count:int,first_position:int,raw?:array}
     */
    public function fetchItemList(
        string $keyword = '',
        int $hits = 20,
        int $offset = 1,
        array $extraParams = [],
        int $ttlSeconds = 3600
    ): array {
        $hits = max(1, min(100, $hits));
        $offset = max(1, $offset);
        $keyword = trim($keyword);

        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'site' => 'FANZA',
            'service' => 'digital',
            'floor' => 'videoa',
            'hits' => $hits,
            'offset' => $offset,
            'sort' => 'rank',
            'output' => 'json',
        ];
        if ($keyword !== '') {
            $query['keyword'] = $keyword;
        }
        foreach ($extraParams as $key => $value) {
            if (!is_string($key) || $key === '') {
                continue;
            }
            if (is_array($value)) {
                $normalized = [];
                foreach ($value as $item) {
                    if (is_scalar($item)) {
                        $normalized[] = (string) $item;
                    }
                }
                if (count($normalized) > 0) {
                    $query[$key] = $normalized;
                }
                continue;
            }
            if (is_scalar($value)) {
                $query[$key] = (string) $value;
            }
        }

        $cacheKey = $this->makeCacheKey('itemlist_', $query);
        $cachePath = $this->cacheDir . '/' . $cacheKey . '.json';

        if ($this->shouldUseDbOnlyMode()) {
            return $this->fetchItemListFromProductDb($query, $hits, $offset);
        }

        if (is_file($cachePath) && (time() - filemtime($cachePath) <= $ttlSeconds)) {
            $cached = json_decode((string) file_get_contents($cachePath), true);
            if (is_array($cached)) {
                $response = $this->buildListResponse($cached, null);
                $this->persistProductsFromItemList($response['items'], $query);
                return $response;
            }
        }

        $url = 'https://api.dmm.com/affiliate/v3/ItemList?' . http_build_query($query);
        $httpError = null;
        $body = $this->httpGet($url, $httpError);
        if ($body === null) {
            $message = 'Failed to request DMM API';
            if (is_string($httpError) && $httpError !== '') {
                $message .= ': ' . $httpError;
            }
            if ($this->productDb !== null) {
                return $this->fetchItemListFromProductDb($query, $hits, $offset);
            }
            return ['items' => [], 'error' => $message, 'total_count' => 0, 'first_position' => $offset];
        }

        // Guard against oversized payloads that can crash json_decode under low memory limits.
        if (strlen($body) > 12 * 1024 * 1024) {
            return [
                'items' => [],
                'error' => 'DMM API response is too large. Narrow your filters and retry.',
                'total_count' => 0,
                'first_position' => $offset,
            ];
        }

        $data = json_decode($body, true);
        if (!is_array($data)) {
            return ['items' => [], 'error' => 'DMM API response is not valid JSON', 'total_count' => 0, 'first_position' => $offset];
        }

        if (!is_dir($this->cacheDir)) {
            @mkdir($this->cacheDir, 0775, true);
        }
        // Cache raw response body to avoid an extra full JSON encode pass (memory heavy on large payloads).
        @file_put_contents($cachePath, $body);

        $response = $this->buildListResponse($data, null);
        $this->persistProductsFromItemList($response['items'], $query);
        return $response;
    }

    /**
     * @param array<string,mixed> $query
     * @return array{items:array<int,array>,error:?string,total_count:int,first_position:int}
     */
    private function fetchItemListFromProductDb(array $query, int $hits, int $offset): array
    {
        if ($this->productDb === null) {
            return ['items' => [], 'error' => 'Product DB unavailable', 'total_count' => 0, 'first_position' => $offset];
        }

        $article = '';
        if (isset($query['article'])) {
            if (is_array($query['article'])) {
                foreach ($query['article'] as $v) {
                    if (is_scalar($v) && trim((string) $v) !== '') {
                        $article = trim((string) $v);
                        break;
                    }
                }
            } elseif (is_scalar($query['article'])) {
                $article = trim((string) $query['article']);
            }
        }
        $articleIdRaw = $query['article_id'] ?? [];
        $articleIds = [];
        if (is_array($articleIdRaw)) {
            foreach ($articleIdRaw as $v) {
                if (is_numeric($v)) {
                    $articleIds[] = (int) $v;
                }
            }
        } elseif (is_scalar($articleIdRaw) && is_numeric($articleIdRaw)) {
            $articleIds[] = (int) $articleIdRaw;
        }
        $articleNames = $this->resolveArticleNames($article, $articleIds);

        $filters = [
            'keyword' => isset($query['keyword']) && is_scalar($query['keyword']) ? (string) $query['keyword'] : '',
            'cid' => isset($query['cid']) && is_scalar($query['cid']) ? (string) $query['cid'] : '',
            'article' => $article,
            'article_names' => $articleNames,
            'sort' => isset($query['sort']) && is_scalar($query['sort']) ? (string) $query['sort'] : 'date',
        ];

        $result = $this->productDb->searchItemsDmmLike($filters, $hits, $offset);
        return [
            'items' => isset($result['items']) && is_array($result['items']) ? $result['items'] : [],
            'error' => null,
            'total_count' => isset($result['total_count']) && is_numeric($result['total_count']) ? (int) $result['total_count'] : 0,
            'first_position' => $offset,
        ];
    }

    /**
     * @param array<int,int> $ids
     * @return array<int,string>
     */
    private function resolveArticleNames(string $article, array $ids): array
    {
        if (count($ids) === 0) {
            return [];
        }
        $table = '';
        if ($article === 'maker') {
            $table = 'makers';
        } elseif ($article === 'series') {
            $table = 'series';
        } elseif ($article === 'genre') {
            $table = 'genres';
        } elseif ($article === 'actress') {
            $table = 'actresses';
        }
        if ($table === '') {
            return [];
        }

        $dbPath = __DIR__ . '/../data/actresses.sqlite';
        try {
            $pdo = new PDO('sqlite:' . $dbPath);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
            $placeholders = [];
            $bind = [];
            foreach (array_values(array_unique($ids)) as $idx => $id) {
                $ph = ':id_' . $idx;
                $placeholders[] = $ph;
                $bind[$ph] = $id;
            }
            if (count($placeholders) === 0) {
                return [];
            }
            $sql = 'SELECT name FROM ' . $table . ' WHERE id IN (' . implode(',', $placeholders) . ')';
            $stmt = $pdo->prepare($sql);
            foreach ($bind as $ph => $id) {
                $stmt->bindValue($ph, $id, PDO::PARAM_INT);
            }
            $stmt->execute();
            $rows = $stmt->fetchAll();
            $out = [];
            foreach ($rows as $row) {
                if (!is_array($row) || !isset($row['name']) || !is_string($row['name'])) {
                    continue;
                }
                $name = trim($row['name']);
                if ($name === '') {
                    continue;
                }
                $out[$name] = true;
            }
            return array_keys($out);
        } catch (Throwable $e) {
            return [];
        }
    }

    private function shouldUseDbOnlyMode(): bool
    {
        $env = getenv('DMM_API_DISABLE');
        if (is_string($env)) {
            $v = strtolower(trim($env));
            if (in_array($v, ['1', 'true', 'yes', 'on'], true)) {
                return true;
            }
        }
        if (PHP_SAPI !== 'cli' && isset($_GET['db_only'])) {
            $q = strtolower(trim((string) $_GET['db_only']));
            if (in_array($q, ['1', 'true', 'yes', 'on'], true)) {
                return true;
            }
        }
        return false;
    }

    private function initProductDb(): void
    {
        try {
            $dbPath = __DIR__ . '/../data/actresses.sqlite';
            $db = new ProductDb($dbPath);
            if (PHP_SAPI === 'cli' || !is_file($dbPath) || filesize($dbPath) === 0) {
                $db->ensureSchema();
            }
            $this->productDb = $db;
        } catch (Throwable $e) {
            // Product DB is optional; API flow must continue even if DB is unavailable.
            $this->productDb = null;
        }
    }

    /**
     * @param array<int,array> $items
     * @param array<string,mixed> $query
     */
    private function persistProductsFromItemList(array $items, array $query): void
    {
        if ($this->productDb === null || count($items) === 0) {
            return;
        }
        $source = $this->resolveProductSource($query);
        try {
            $this->productDb->upsertItems($items, $source);
        } catch (Throwable $e) {
            // Ignore DB write failures to avoid impacting page availability.
        }
    }

    /**
     * @param array<string,mixed> $query
     */
    private function resolveProductSource(array $query): string
    {
        if (isset($query['cid']) && is_string($query['cid']) && trim($query['cid']) !== '') {
            return 'itemlist_cid';
        }
        $sort = 'rank';
        if (isset($query['sort']) && is_scalar($query['sort'])) {
            $sortRaw = strtolower(trim((string) $query['sort']));
            if (in_array($sortRaw, ['rank', 'date'], true)) {
                $sort = $sortRaw;
            }
        }
        $hasArticle = isset($query['article']) || isset($query['article_id']);
        $hasKeyword = isset($query['keyword']) && is_scalar($query['keyword']) && trim((string) $query['keyword']) !== '';
        if ($hasArticle) {
            return 'itemlist_' . $sort . '_article';
        }
        if ($hasKeyword) {
            return 'itemlist_' . $sort . '_keyword';
        }
        return 'itemlist_' . $sort;
    }

    private function httpGet(string $url, ?string &$error = null): ?string
    {
        $error = null;
        if (function_exists('curl_init')) {
            $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_SSL_VERIFYHOST => 2,
                CURLOPT_USERAGENT => 'mixhost-fanza-site/1.0',
            ]);
            $body = curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $curlError = curl_error($ch);
            curl_close($ch);
            if (is_string($body) && $status >= 200 && $status < 300) {
                return $body;
            }
            if ($curlError !== '') {
                $error = $curlError;
            } else {
                $error = 'HTTP status ' . $status;
            }
            return null;
        }

        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'timeout' => 20,
                'header' => "User-Agent: mixhost-fanza-site/1.0\r\n",
            ],
        ]);
        $body = @file_get_contents($url, false, $context);
        if (is_string($body)) {
            return $body;
        }
        $lastError = error_get_last();
        if (is_array($lastError) && isset($lastError['message']) && is_string($lastError['message'])) {
            $error = $lastError['message'];
        } else {
            $error = 'file_get_contents failed';
        }
        return null;
    }

    private function extractFirstItem(array $data): ?array
    {
        if (!isset($data['result']['items']) || !is_array($data['result']['items'])) {
            return null;
        }
        if (!isset($data['result']['items'][0]) || !is_array($data['result']['items'][0])) {
            return null;
        }
        return $data['result']['items'][0];
    }

    private function makeCacheKey(string $prefix, array $query): string
    {
        ksort($query);
        $normalized = json_encode($query, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $hash = sha1((string) $normalized);
        return $prefix . $hash;
    }

    /**
     * @return array{items:array<int,array>,error:?string,total_count:int,first_position:int,raw?:array}
     */
    private function buildListResponse(array $data, ?string $error): array
    {
        $items = [];
        if (isset($data['result']['items']) && is_array($data['result']['items'])) {
            foreach ($data['result']['items'] as $item) {
                if (is_array($item)) {
                    $items[] = $item;
                }
            }
        }
        $totalCount = 0;
        if (isset($data['result']['total_count']) && is_numeric($data['result']['total_count'])) {
            $totalCount = (int) $data['result']['total_count'];
        }
        $firstPosition = 1;
        if (isset($data['result']['first_position']) && is_numeric($data['result']['first_position'])) {
            $firstPosition = (int) $data['result']['first_position'];
        }
        return [
            'items' => $items,
            'error' => $error,
            'total_count' => $totalCount,
            'first_position' => $firstPosition,
        ];
    }

    /**
     * @param array<string,mixed> $filters
     * @return array{actresses:array<int,array>,error:?string,raw?:array}
     */
    public function fetchActressList(array $filters = [], int $ttlSeconds = 3600): array
    {
        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'hits' => 100,
            'offset' => 1,
            'output' => 'json',
        ];
        foreach ($filters as $key => $value) {
            if (!is_string($key) || $key === '') {
                continue;
            }
            if (is_scalar($value) && (string) $value !== '') {
                $query[$key] = (string) $value;
            }
        }

        $cacheKey = $this->makeCacheKey('actress_', $query);
        $cachePath = $this->cacheDir . '/' . $cacheKey . '.json';
        if (is_file($cachePath) && (time() - filemtime($cachePath) <= $ttlSeconds)) {
            $cached = json_decode((string) file_get_contents($cachePath), true);
            if (is_array($cached)) {
                return ['actresses' => $this->extractActresses($cached), 'error' => null, 'raw' => $cached];
            }
        }

        $url = 'https://api.dmm.com/affiliate/v3/ActressSearch?' . http_build_query($query);
        $httpError = null;
        $body = $this->httpGet($url, $httpError);
        if ($body === null) {
            $message = 'Failed to request ActressSearch API';
            if (is_string($httpError) && $httpError !== '') {
                $message .= ': ' . $httpError;
            }
            return ['actresses' => [], 'error' => $message];
        }
        $data = json_decode($body, true);
        if (!is_array($data)) {
            return ['actresses' => [], 'error' => 'ActressSearch response is not valid JSON'];
        }
        if (!is_dir($this->cacheDir)) {
            @mkdir($this->cacheDir, 0775, true);
        }
        @file_put_contents($cachePath, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

        return ['actresses' => $this->extractActresses($data), 'error' => null, 'raw' => $data];
    }

    /**
     * @return array{actress:?array,error:?string,raw?:array}
     */
    public function fetchActressByKeyword(string $keyword, int $ttlSeconds = 3600): array
    {
        $keyword = trim($keyword);
        if ($keyword === '') {
            return ['actress' => null, 'error' => null];
        }
        $list = $this->fetchActressList(['keyword' => $keyword], $ttlSeconds);
        if ($list['error'] !== null) {
            return ['actress' => null, 'error' => $list['error']];
        }
        return ['actress' => $this->pickActressFromList($list['actresses'], $keyword), 'error' => null, 'raw' => $list['raw'] ?? null];
    }

    /**
     * @return array<int,array>
     */
    private function extractActresses(array $data): array
    {
        if (!isset($data['result']['actress']) || !is_array($data['result']['actress'])) {
            return [];
        }
        $actresses = [];
        foreach ($data['result']['actress'] as $row) {
            if (is_array($row)) {
                $actresses[] = $row;
            }
        }
        return $actresses;
    }

    private function pickActressFromList(array $actresses, string $keyword): ?array
    {
        if (count($actresses) === 0) {
            return null;
        }
        foreach ($actresses as $row) {
            if (isset($row['name']) && is_string($row['name']) && strtolower($row['name']) === strtolower($keyword)) {
                return $row;
            }
        }
        return $actresses[0];
    }

    /**
     * @return array{makers:array<int,array>,error:?string,raw?:array}
     */
    public function fetchMakerList(int $hits = 500, int $offset = 1, ?int $floorId = null, int $ttlSeconds = 3600): array
    {
        $hits = max(1, min(500, $hits));
        $offset = max(1, $offset);
        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'hits' => $hits,
            'offset' => $offset,
            'output' => 'json',
        ];
        if ($floorId !== null && $floorId > 0) {
            $query['floor_id'] = $floorId;
        }
        return $this->fetchLookup('/affiliate/v3/MakerSearch', 'maker', 'makers', $query, $ttlSeconds);
    }

    /**
     * @return array{series:array<int,array>,error:?string,raw?:array}
     */
    public function fetchSeriesList(int $hits = 500, int $offset = 1, ?int $floorId = null, int $ttlSeconds = 3600): array
    {
        $hits = max(1, min(500, $hits));
        $offset = max(1, $offset);
        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'hits' => $hits,
            'offset' => $offset,
            'output' => 'json',
        ];
        if ($floorId !== null && $floorId > 0) {
            $query['floor_id'] = $floorId;
        }
        return $this->fetchLookup('/affiliate/v3/SeriesSearch', 'series', 'series', $query, $ttlSeconds);
    }

    /**
     * @return array{genres:array<int,array>,error:?string,raw?:array}
     */
    public function fetchGenreList(int $hits = 500, int $offset = 1, ?int $floorId = null, int $ttlSeconds = 3600): array
    {
        $hits = max(1, min(500, $hits));
        $offset = max(1, $offset);
        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'hits' => $hits,
            'offset' => $offset,
            'output' => 'json',
        ];
        if ($floorId !== null && $floorId > 0) {
            $query['floor_id'] = $floorId;
        }
        return $this->fetchLookup('/affiliate/v3/GenreSearch', 'genre', 'genres', $query, $ttlSeconds);
    }

    public function resolveFloorId(string $siteCode = 'FANZA', string $serviceCode = 'digital', string $floorCode = 'videoa', int $ttlSeconds = 86400): ?int
    {
        if ($this->floorIdCache !== null) {
            return $this->floorIdCache;
        }

        $query = [
            'api_id' => $this->apiId,
            'affiliate_id' => $this->affiliateId,
            'output' => 'json',
        ];
        $cacheKey = $this->makeCacheKey('floor_', $query);
        $cachePath = $this->cacheDir . '/' . $cacheKey . '.json';
        $data = null;

        if (is_file($cachePath) && (time() - filemtime($cachePath) <= $ttlSeconds)) {
            $cached = json_decode((string) file_get_contents($cachePath), true);
            if (is_array($cached)) {
                $data = $cached;
            }
        }

        if (!is_array($data)) {
            $url = 'https://api.dmm.com/affiliate/v3/FloorList?' . http_build_query($query);
            $httpError = null;
            $body = $this->httpGet($url, $httpError);
            if ($body === null) {
                return null;
            }
            $decoded = json_decode($body, true);
            if (!is_array($decoded)) {
                return null;
            }
            $data = $decoded;
            if (!is_dir($this->cacheDir)) {
                @mkdir($this->cacheDir, 0775, true);
            }
            @file_put_contents($cachePath, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }

        if (!isset($data['result']['site']) || !is_array($data['result']['site'])) {
            return null;
        }
        foreach ($data['result']['site'] as $site) {
            if (!is_array($site)) {
                continue;
            }
            $sc = $site['site_code'] ?? '';
            if (!is_string($sc) || strtolower($sc) !== strtolower($siteCode)) {
                continue;
            }
            if (!isset($site['service']) || !is_array($site['service'])) {
                continue;
            }
            foreach ($site['service'] as $service) {
                if (!is_array($service)) {
                    continue;
                }
                $svc = $service['service_code'] ?? '';
                if (!is_string($svc) || strtolower($svc) !== strtolower($serviceCode)) {
                    continue;
                }
                if (!isset($service['floor']) || !is_array($service['floor'])) {
                    continue;
                }
                foreach ($service['floor'] as $floor) {
                    if (!is_array($floor)) {
                        continue;
                    }
                    $fc = $floor['floor_code'] ?? '';
                    if (!is_string($fc) || strtolower($fc) !== strtolower($floorCode)) {
                        continue;
                    }
                    if (isset($floor['floor_id']) && is_numeric($floor['floor_id'])) {
                        $this->floorIdCache = (int) $floor['floor_id'];
                        return $this->floorIdCache;
                    }
                }
            }
        }
        return null;
    }

    /**
     * @return array{makers?:array<int,array>,series?:array<int,array>,genres?:array<int,array>,error:?string,raw?:array}
     */
    private function fetchLookup(string $path, string $itemKey, string $resultKey, array $query, int $ttlSeconds): array
    {
        $cacheKey = $this->makeCacheKey($itemKey . '_', $query);
        $cachePath = $this->cacheDir . '/' . $cacheKey . '.json';

        if (is_file($cachePath) && (time() - filemtime($cachePath) <= $ttlSeconds)) {
            $cached = json_decode((string) file_get_contents($cachePath), true);
            if (is_array($cached)) {
                return [$resultKey => $this->extractLookupItems($cached, $itemKey), 'error' => null, 'raw' => $cached];
            }
        }

        $url = 'https://api.dmm.com' . $path . '?' . http_build_query($query);
        $httpError = null;
        $body = $this->httpGet($url, $httpError);
        if ($body === null) {
            $message = 'Failed to request ' . $itemKey . ' API';
            if (is_string($httpError) && $httpError !== '') {
                $message .= ': ' . $httpError;
            }
            return [$resultKey => [], 'error' => $message];
        }
        $data = json_decode($body, true);
        if (!is_array($data)) {
            return [$resultKey => [], 'error' => 'Invalid JSON for ' . $itemKey . ' API'];
        }
        if (!is_dir($this->cacheDir)) {
            @mkdir($this->cacheDir, 0775, true);
        }
        @file_put_contents($cachePath, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

        return [$resultKey => $this->extractLookupItems($data, $itemKey), 'error' => null, 'raw' => $data];
    }

    /**
     * @return array<int,array>
     */
    private function extractLookupItems(array $data, string $itemKey): array
    {
        if (!isset($data['result'][$itemKey]) || !is_array($data['result'][$itemKey])) {
            return [];
        }
        $rows = [];
        foreach ($data['result'][$itemKey] as $row) {
            if (is_array($row)) {
                $rows[] = $row;
            }
        }
        return $rows;
    }
}
