<?php

namespace app\service\AdminConsole;

use InvalidArgumentException;
use support\Db;

class ArchiveAdminService
{
    public function list(string $query = '', int $page = 1, int $pageSize = 20): array
    {
        $page = max(1, $page);
        $pageSize = min(100, max(1, $pageSize));

        $builder = Db::table('archives');
        $query = trim($query);
        if ($query !== '') {
            $like = '%' . $query . '%';
            $builder->where(function ($subQuery) use ($like): void {
                $subQuery
                    ->orWhere('archive_uid', 'like', $like)
                    ->orWhere('title', 'like', $like)
                    ->orWhere('summary', 'like', $like)
                    ->orWhere('author', 'like', $like)
                    ->orWhere('source', 'like', $like)
                    ->orWhere('series', 'like', $like);
            });
        }

        $total = (clone $builder)->count();
        $rows = $builder
            ->orderByDesc('updated_time')
            ->offset(($page - 1) * $pageSize)
            ->limit($pageSize)
            ->get([
                'archive_uid',
                'title',
                'summary',
                'year',
                'author',
                'source',
                'series',
                'tags',
                'created_time',
                'updated_time',
                Db::raw('jsonb_array_length(chunks) as chunk_count'),
            ])
            ->all();

        return [
            'items' => array_map(fn (object $row): array => $this->listItem($row), $rows),
            'total' => (int) $total,
            'page' => $page,
            'page_size' => $pageSize,
        ];
    }

    public function detail(string $archiveUid): ?array
    {
        $row = Db::table('archives')->where('archive_uid', $archiveUid)->first();
        if (!$row) {
            return null;
        }

        return $this->detailItem($row);
    }

    public function update(string $archiveUid, array $payload): ?array
    {
        if (!$this->detail($archiveUid)) {
            return null;
        }

        $updates = [];
        foreach (['title', 'summary', 'author', 'source', 'series'] as $field) {
            if (array_key_exists($field, $payload)) {
                $updates[$field] = $this->nullableText($payload[$field]);
            }
        }

        if (array_key_exists('year', $payload)) {
            $year = trim((string) ($payload['year'] ?? ''));
            if ($year === '') {
                $updates['year'] = null;
            } elseif (!preg_match('/^\d{1,4}$/', $year)) {
                throw new InvalidArgumentException('year must be empty or a 1-4 digit number.');
            } else {
                $updates['year'] = (int) $year;
            }
        }

        if (array_key_exists('tags', $payload)) {
            $updates['tags'] = json_encode($this->normalizeTags($payload['tags']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }

        if (array_key_exists('metadata', $payload)) {
            $updates['metadata'] = json_encode($this->normalizeMetadata($payload['metadata']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }

        if ($updates !== []) {
            Db::table('archives')->where('archive_uid', $archiveUid)->update($updates);
        }

        return $this->detail($archiveUid);
    }

    public function delete(string $archiveUid): bool
    {
        return (int) Db::table('archives')->where('archive_uid', $archiveUid)->delete() > 0;
    }

    private function listItem(object $row): array
    {
        $chunks = $this->decodeJson($row->chunks ?? null, []);

        return [
            'archive_uid' => (string) $row->archive_uid,
            'title' => $row->title,
            'summary' => $row->summary,
            'year' => $row->year === null ? null : (int) $row->year,
            'author' => $row->author,
            'source' => $row->source,
            'series' => $row->series,
            'tags' => $this->decodeJson($row->tags ?? null, []),
            'chunk_count' => property_exists($row, 'chunk_count')
                ? ($row->chunk_count === null ? 0 : (int) $row->chunk_count)
                : count(is_array($chunks) ? $chunks : []),
            'created_time' => $row->created_time,
            'updated_time' => $row->updated_time,
        ];
    }

    private function detailItem(object $row): array
    {
        $data = $this->listItem($row);
        $data['metadata'] = $this->decodeJson($row->metadata ?? null, []);
        $data['chunks'] = $this->decodeJson($row->chunks ?? null, []);

        return $data;
    }

    private function normalizeTags(mixed $value): array
    {
        if (is_array($value)) {
            $items = $value;
        } else {
            $text = trim((string) $value);
            if ($text === '') {
                return [];
            }
            $items = preg_split('/[\r\n,]+/', $text) ?: [];
        }

        $tags = [];
        foreach ($items as $item) {
            $tag = trim((string) $item);
            if ($tag !== '') {
                $tags[] = $tag;
            }
        }

        return array_values(array_unique($tags));
    }

    private function normalizeMetadata(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        $text = trim((string) $value);
        if ($text === '') {
            return [];
        }

        $decoded = json_decode($text, true);
        if (!is_array($decoded)) {
            throw new InvalidArgumentException('metadata must be a JSON object or array.');
        }

        return $decoded;
    }

    private function nullableText(mixed $value): ?string
    {
        $text = trim((string) $value);
        return $text === '' ? null : $text;
    }

    private function decodeJson(mixed $value, mixed $fallback): mixed
    {
        if ($value === null) {
            return $fallback;
        }

        if (is_array($value)) {
            return $value;
        }

        $decoded = json_decode((string) $value, true);
        return $decoded === null && json_last_error() !== JSON_ERROR_NONE ? $fallback : $decoded;
    }
}
