<?php

namespace App\Filament\Pages;

use App\Models\MarkdownImport;
use App\Models\SourcePaper;
use App\Models\Textbook;
use App\Models\TextbookCatalog;
use App\Services\ImportInferenceService;
use App\Models\TextbookSeries;
use Filament\Pages\Page;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Log;

class MarkdownImportWorkbench extends Page
{
    protected static ?string $navigationLabel = '导入工作台';
    protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
    protected static string|\UnitEnum|null $navigationGroup = '卷子导入流程';
    protected static ?int $navigationSort = 2;

    protected string $view = 'filament.pages.markdown-import-workbench';

    protected ?ImportInferenceService $inferenceService = null;

    protected function inferenceService(): ImportInferenceService
    {
        return $this->inferenceService ??= app(ImportInferenceService::class);
    }

    public ?int $importId = null;
    public ?int $selectedPaperId = null;
    public array $selectedIds = [];
    public string $search = '';
    public string $groupBy = 'paper';
    public bool $dense = false;
    public bool $filenameValid = true;
    public array $filenameParsed = [];
    public ?string $filenameWarning = null;

    public array $form = [
        'title' => null,
        'edition' => null,
        'grade' => null,
        'term' => null,
        'chapter' => null,
        'source_type' => null,
        'source_year' => null,
        'textbook_id' => null,
        'textbook_series' => null,
        'textbook_series_id' => null,
        'source_name' => null,
        'source_page' => null,
        'subject' => '数学', // 默认学科
        'tags' => '',
        'bundle_key' => null,
        'expected_count' => null,
        'catalog_node_ids' => [],
    ];

    public array $batch = [
        'edition' => null,
        'grade' => null,
        'term' => null,
        'chapter' => null,
        'source_type' => null,
        'source_year' => null,
        'textbook_id' => null,
        'textbook_series' => null,
        'source_name' => null,
        'source_page' => null,
        'tags' => '',
        'bundle_key' => null,
        'expected_count' => null,
        'catalog_node_ids' => [],
    ];

    private array $catalogNodeCache = [];

    public function mount(): void
    {
        $this->importId = request()->integer('import_id');

        $parsed = $this->parseImportFilename();
        if (empty($parsed)) {
            $this->filenameValid = false;
            $this->filenameWarning = '文件名不符合规范：系列_年级_学期_学科_名称（例：北师大版_7_1_数学_...）';
        } else {
            $this->filenameParsed = $parsed;
            $this->applyFilenameDefaults();
        }

        $first = $this->papers()->first();
        if ($first) {
            $this->selectPaper($first->id);
        }
    }

    public function importRecord(): ?MarkdownImport
    {
        return $this->importId ? MarkdownImport::query()->find($this->importId) : null;
    }

    public function papers()
    {
        if (!$this->importId) {
            return collect();
        }

        $query = SourcePaper::query()
            ->whereHas('candidates', fn ($q) => $q->where('import_id', $this->importId))
            ->withCount(['candidates', 'parts'])
            ->orderBy('order');

        if ($this->search !== '') {
            $query->where(function ($q) {
                $q->where('title', 'like', '%' . $this->search . '%')
                    ->orWhere('full_title', 'like', '%' . $this->search . '%')
                    ->orWhere('paper_code', 'like', '%' . $this->search . '%');
            });
        }

        return $query->get();
    }

    public function groupedPapers(): array
    {
        $papers = $this->papers();

        if ($this->groupBy === 'paper') {
            return $papers->groupBy(fn ($paper) => $paper->title ?: $paper->full_title ?: '未命名卷子')->toArray();
        }

        if ($this->groupBy === 'grade') {
            return $papers->groupBy(fn ($paper) => $paper->grade ? $paper->grade . '年级' : '未标注年级')->toArray();
        }

        return $papers->groupBy(fn ($paper) => Arr::get($paper->meta ?? [], 'bundle_key', '未归并套卷'))->toArray();
    }

    public function selectedPaper(): ?SourcePaper
    {
        return $this->selectedPaperId
            ? SourcePaper::query()->withCount(['candidates', 'parts'])->find($this->selectedPaperId)
            : null;
    }

    public function selectPaper(int $paperId): void
    {
        $paper = SourcePaper::query()->find($paperId);
        if (!$paper) {
            return;
        }

        Log::info('MarkdownImportWorkbench select paper', [
            'import_id' => $this->importId,
            'paper_id' => $paperId,
            'paper_title' => $paper->title,
            'selected_ids' => $this->selectedIds,
        ]);

        $parsed = $this->parseImportFilename();
        $this->selectedPaperId = $paperId;
        $meta = $paper->meta ?? [];

        // 核心修正：直接基于物理 series_id 初始化，如果没有则尝试从文件名或标题解析
        $finalSeriesId = $paper->series_id;
        $finalSeriesName = null;

        if (!$finalSeriesId) {
            // 兜底1：从文件名解析
            if (!empty($parsed['series'])) {
                $formalSeries = $this->inferenceService()->resolveSeries($parsed['series']);
                $finalSeriesId = $formalSeries?->id;
                $finalSeriesName = $formalSeries ? $formalSeries->name : $parsed['series'];
            }
            
            // 兜底2：从标题模糊推断 (应对数据库数据未对齐的情况)
            if (!$finalSeriesId) {
                $title = $paper->full_title ?: $paper->title;
                $formalSeries = $this->inferenceService()->resolveSeries($title);
                $finalSeriesId = $formalSeries?->id;
                $finalSeriesName = $formalSeries?->name;
            }
        }

        if ($finalSeriesId && !$finalSeriesName) {
            $finalSeriesName = TextbookSeries::find($finalSeriesId)?->name;
        }

        $resolvedTextbook = null;
        if (!$paper->textbook_id && $finalSeriesId) {
            $resolvedTextbook = $this->inferenceService()->findBestTextbook([
                'series_id' => $finalSeriesId,
                'grade' => $paper->grade ?: ($parsed['grade'] ?? null),
                'term' => $paper->term ?: ($parsed['term'] ?? null),
            ]);
        }

        $initialCatalogIds = (array) (Arr::get($meta, 'catalog_node_ids') ?: (Arr::get($meta, 'catalog_node_id') ? [Arr::get($meta, 'catalog_node_id')] : []));
        $filteredCatalogIds = $this->filterCatalogNodeIdsForTextbook($paper->textbook_id ?: ($resolvedTextbook?->id ?? null), $initialCatalogIds);
        if ($initialCatalogIds !== $filteredCatalogIds && !empty($initialCatalogIds)) {
            Log::warning('MarkdownImportWorkbench catalog nodes not in textbook', [
                'paper_id' => $paper->id,
                'paper_title' => $paper->title,
                'textbook_id' => $paper->textbook_id,
                'original_catalog_node_ids' => $initialCatalogIds,
                'filtered_catalog_node_ids' => $filteredCatalogIds,
            ]);
        }

        $this->form = [
            'title' => $paper->title,
            'edition' => $paper->edition,
            'grade' => $paper->grade ?: ($parsed['grade'] ?? null),
            'term' => $paper->term ?: ($parsed['term'] ?? null),
            'chapter' => $paper->chapter,
            'source_type' => $paper->source_type,
            'source_year' => $paper->source_year,
            'textbook_id' => $paper->textbook_id ?: ($resolvedTextbook?->id ?? null),
            'textbook_series' => $finalSeriesName,
            'textbook_series_id' => $finalSeriesId,
            'source_name' => Arr::get($meta, 'source_name') ?: ($parsed['name'] ?? null),
            'source_page' => Arr::get($meta, 'source_page'),
            'tags' => implode(',', Arr::get($meta, 'tags', [])),
            'bundle_key' => Arr::get($meta, 'bundle_key'),
            'expected_count' => Arr::get($meta, 'expected_count'),
            'catalog_node_ids' => $filteredCatalogIds,
        ];
    }

    public function updatedSelectedIds(): void
    {
        if (empty($this->selectedIds) || $this->selectedPaperId === null) {
            return;
        }

        if ($this->isBatchEmpty()) {
            $this->seedBatchFromCurrent();
        }
    }

    public function updated($name, $value): void
    {
        if (!$this->selectedPaperId) {
            return;
        }
    }

    /**
     * 教材属性联动：当系列、年级、学期改变时，自动匹配最合适的教材
     */
    public function updatedFormGrade(): void { $this->reInferTextbook(); }
    public function updatedFormTerm(): void { $this->reInferTextbook(); }
    public function updatedFormTextbookSeriesId(): void 
    { 
        // 核心联动：系列 ID 变动，清空教材并重推
        $this->form['textbook_id'] = null;
        $this->form['catalog_node_ids'] = [];
        
        // 同时同步一下显示名称 (虽然逻辑以 ID 为准，但保留名称用于前端显示或保存)
        $series = TextbookSeries::find($this->form['textbook_series_id']);
        $this->form['textbook_series'] = $series?->name;
        
        $this->reInferTextbook(); 
    }

    protected function reInferTextbook(): void
    {
        if (!empty($this->form['textbook_id'])) {
            return;
        }

        $best = $this->inferenceService()->findBestTextbook($this->form);
        if ($best) {
            // 无论 ID 是否改变，都强制执行一次属性校准，确保“一环扣一环”
            $this->syncTextbookAttributes($best);
        }
    }

    protected function syncTextbookAttributes(Textbook $textbook): void
    {
        $this->form['textbook_id'] = $textbook->id;
        $this->form['grade'] = (string)$textbook->grade;
        $this->form['term'] = $this->semesterToTerm($textbook->semester);
        $this->form['textbook_series_id'] = $textbook->series_id;
        
        $series = $textbook->getRelation('series') ?: $textbook->series()->first();
        $seriesName = $textbook->track ?: ($series?->name ?? null);
        if ($seriesName) {
            $this->form['textbook_series'] = $seriesName;
        }
        
        $this->savePaper();
    }

    public function updatedFormTextbookId($value): void
    {
        if (!$this->selectedPaperId || !$value) {
            return;
        }

        // 权威源：从数据库获取该教材的所有官方属性
        $textbook = Textbook::query()->with('series')->find($value);
        if ($textbook) {
            $this->syncTextbookAttributes($textbook);
        }
    }

    protected function semesterToTerm(?int $semester): ?string
    {
        return match ($semester) {
            1 => '上册',
            2 => '下册',
            default => null,
        };
    }

    public function seedBatchFromCurrent(): void
    {
        $this->batch = [
            'edition' => $this->form['edition'] ?? null,
            'grade' => $this->form['grade'] ?? null,
            'term' => $this->form['term'] ?? null,
            'chapter' => $this->form['chapter'] ?? null,
            'source_type' => $this->form['source_type'] ?? null,
            'source_year' => $this->form['source_year'] ?? null,
            'textbook_id' => $this->form['textbook_id'] ?? null,
            'textbook_series_id' => $this->form['textbook_series_id'] ?? null,
            'textbook_series' => $this->form['textbook_series'] ?? null,
            'source_name' => $this->form['source_name'] ?? null,
            'source_page' => $this->form['source_page'] ?? null,
            'tags' => $this->form['tags'] ?? '',
            'bundle_key' => $this->form['bundle_key'] ?? null,
            'expected_count' => $this->form['expected_count'] ?? null,
            'catalog_node_ids' => $this->form['catalog_node_ids'] ?? [],
        ];
    }

    public function savePaper(bool $silent = false): void
    {
        $paper = $this->selectedPaper();
        if (!$paper) {
            Log::warning('MarkdownImportWorkbench save failed: no paper selected', [
                'import_id' => $this->importId,
                'selected_paper_id' => $this->selectedPaperId,
            ]);
            return;
        }

        $selectedIds = array_values(array_filter(array_unique(array_map('intval', $this->selectedIds))));

        $meta = $paper->meta ?? [];
        $meta['source_name'] = $this->form['source_name'] ?? null;
        $meta['source_page'] = $this->form['source_page'] ?? null;
        $meta['tags'] = $this->explodeTags($this->form['tags'] ?? '');
        $meta['bundle_key'] = $this->form['bundle_key'] ?? null;
        $meta['expected_count'] = $this->form['expected_count'] ?? null;
        
        // 关键修正：确保目录 ID 是干净的整型数组，解决保存失效
        $catalogNodeIds = $this->normalizeCatalogNodeIds($this->form['catalog_node_ids'] ?? []);
        $catalogNodeIds = $this->filterCatalogNodeIdsForTextbook($updates['textbook_id'] ?? null, $catalogNodeIds);
        $meta['catalog_node_ids'] = $catalogNodeIds;
        
        // 同时保留旧字段供向后兼容
        $meta['catalog_node_id'] = !empty($meta['catalog_node_ids']) ? $meta['catalog_node_ids'][0] : null;

        // 核心同步：保存时根据 series_id 确保名称同步
        if (!empty($this->form['textbook_series_id'])) {
            $series = TextbookSeries::find($this->form['textbook_series_id']);
            if ($series) {
                $this->form['textbook_series'] = $series->name;
            }
        }

        $fields = [
            'title', 'edition', 'grade', 'term', 'chapter', 'source_type', 
            'source_year', 'textbook_id'
        ];
        
        $updates = [];
        foreach ($fields as $field) {
            $value = $this->form[$field] ?? null;
            $updates[$field] = ($value === '' ? null : $value);
        }
        $updates['series_id'] = $this->form['textbook_series_id'] ?? null;
        $updates['meta'] = $meta;

        Log::info('MarkdownImportWorkbench saving paper', [
            'import_id' => $this->importId,
            'paper_id' => $paper->id,
            'paper_title' => $paper->title,
            'catalog_node_ids' => $catalogNodeIds,
            'textbook_id' => $updates['textbook_id'] ?? null,
            'series_id' => $updates['series_id'] ?? null,
        ]);

        $paper->update($updates);

        Log::info('MarkdownImportWorkbench saved paper', [
            'import_id' => $this->importId,
            'paper_id' => $paper->id,
            'meta_catalog_node_ids' => $meta['catalog_node_ids'] ?? [],
        ]);

        if (count($selectedIds) > 1) {
            $otherIds = array_values(array_diff($selectedIds, [$paper->id]));
            if (!empty($otherIds)) {
                $this->seedBatchFromCurrent();
                $this->applyBatchToIds($otherIds);

                Log::info('MarkdownImportWorkbench batch saved papers', [
                    'import_id' => $this->importId,
                    'paper_ids' => $otherIds,
                    'catalog_node_ids' => $this->normalizeCatalogNodeIds($this->batch['catalog_node_ids'] ?? []),
                ]);
            }
        }

        $this->selectedIds = [];

        if (!$silent) {
            Notification::make()
                ->title('保存成功')
                ->success()
                ->send();
        }
    }


    public function applyBatch(): void
    {
        if (empty($this->selectedIds)) {
            return;
        }

        $this->applyBatchToIds($this->selectedIds);
    }

    public function autoInfer(): void
    {
        $paper = $this->selectedPaper();
        if (!$paper) {
            return;
        }

        $parsed = $this->parseImportFilename();
        if (!empty($parsed)) {
            // 关键：推断出的系列必须经过正式化 ID 锁定，否则无法触发联动
            if (empty($this->form['textbook_series_id']) && !empty($parsed['series'])) {
                $formal = $this->inferenceService()->resolveSeries($parsed['series']);
                if ($formal) {
                    $this->form['textbook_series_id'] = $formal->id;
                    $this->form['textbook_series'] = $formal->name;
                } else {
                    $this->form['textbook_series'] = $parsed['series'];
                }
            }
            
            $this->form['grade'] = $this->form['grade'] ?: $parsed['grade'];
            $this->form['term'] = $this->form['term'] ?: $parsed['term'];
            $this->form['source_name'] = $this->form['source_name'] ?: $parsed['name'];
        }

        $title = (string) ($paper->title ?? $paper->full_title ?? '');
        $raw = (string) ($paper->raw_markdown ?? '');
        $context = $title . ' ' . $raw;

        $this->form['term'] = $this->inferenceService()->inferTerm($context) ?? $this->form['term'];
        $this->form['grade'] = $this->inferenceService()->inferGrade($context) ?? $this->form['grade'];
        $this->form['chapter'] = $this->inferenceService()->inferChapter($context) ?? $this->form['chapter'];
        $this->form['source_type'] = $this->inferenceService()->inferSourceType($context) ?? $this->form['source_type'];

        if (empty($this->form['catalog_node_ids'])) {
            $matchedCatalog = $this->inferenceService()->matchCatalogNodeId($context, $this->form['textbook_id']);
            if ($matchedCatalog) {
                $this->form['catalog_node_ids'] = [$matchedCatalog];
            }
        }

        // 执行推断后，立即触发教材重新关联和保存，确保“一环扣一环”
        $this->reInferTextbook();
        $this->savePaper();
    }

    public function autoBundleKey(): void
    {
        $paper = $this->selectedPaper();
        if (!$paper) {
            return;
        }

        $this->form['bundle_key'] = $this->buildBundleKey($paper);
    }

    public function autoBundleKeySelected(): void
    {
        if (empty($this->selectedIds)) {
            return;
        }

        foreach (SourcePaper::query()->whereIn('id', $this->selectedIds)->get() as $paper) {
            $meta = $paper->meta ?? [];
            $meta['bundle_key'] = $this->buildBundleKey($paper);
            $paper->update(['meta' => $meta]);
        }
    }

    public function autoInferSelected(): void
    {
        if (empty($this->selectedIds)) {
            return;
        }

        $parsed = $this->parseImportFilename();
        foreach (SourcePaper::query()->whereIn('id', $this->selectedIds)->get() as $paper) {
            $context = (string) ($paper->title ?? $paper->full_title ?? '') . ' ' . (string) ($paper->raw_markdown ?? '');
            $updates = array_filter([
                'term' => $this->inferenceService()->inferTerm($context),
                'grade' => $this->inferenceService()->inferGrade($context),
                'chapter' => $this->inferenceService()->inferChapter($context),
            ], fn ($value) => $value !== null && $value !== '');

            if (!empty($parsed)) {
                if (empty($paper->textbook_series) && !empty($parsed['series'])) {
                    $updates['textbook_series'] = $parsed['series'];
                }
                if (empty($paper->grade) && !empty($parsed['grade'])) {
                    $updates['grade'] = $parsed['grade'];
                }
                if (empty($paper->term) && !empty($parsed['term'])) {
                    $updates['term'] = $parsed['term'];
                }
            }

            $meta = $paper->meta ?? [];
            if (!empty($parsed['name']) && empty($meta['source_name'])) {
                $meta['source_name'] = $parsed['name'];
            }
            if (empty($meta['catalog_node_ids'])) {
                $candidateTextbookId = $updates['textbook_id'] ?? $paper->textbook_id ?? null;
                
                // 如果没有教材 ID，实时推断一个
                if (!$candidateTextbookId) {
                    $best = $this->inferenceService()->findBestTextbook([
                        'series_id' => $updates['textbook_series_id'] ?? null,
                        'grade' => $updates['grade'] ?? $paper->grade,
                        'term' => $updates['term'] ?? $paper->term,
                    ]);
                    if ($best) {
                        $candidateTextbookId = $best->id;
                        $updates['textbook_id'] = $best->id;
                    }
                }

                $matchedCatalog = $this->inferenceService()->matchCatalogNodeId($context, $candidateTextbookId);
                if ($matchedCatalog) {
                    $meta['catalog_node_ids'] = [$matchedCatalog];
                    $meta['catalog_node_id'] = $matchedCatalog;
                }
            }

            if (!empty($updates)) {
                $updates['meta'] = $meta;
                $paper->update($updates);
            } elseif (!empty($meta)) {
                $paper->update(['meta' => $meta]);
            }
        }
    }
    
    public function mergeSelectedPapers(): void
    {
        if (count($this->selectedIds) < 2) {
            $this->dispatch('notify', ['type' => 'warning', 'message' => '请至少选择两套卷子进行合并']);
            return;
        }

        $papers = SourcePaper::query()
            ->whereIn('id', $this->selectedIds)
            ->orderBy('order')
            ->get();

        $target = $papers->shift(); // 第一套作为目标
        
        DB::transaction(function () use ($target, $papers) {
            foreach ($papers as $source) {
                // 1. 移动所有候选题目
                DB::table('pre_question_candidates')
                    ->where('source_paper_id', $source->id)
                    ->update(['source_paper_id' => $target->id]);

                // 2. 移动所有 PaperPart (如果需要)
                DB::table('paper_parts')
                    ->where('source_paper_id', $source->id)
                    ->update(['source_paper_id' => $target->id]);

                // 3. 追加 Markdown 内容 (可选，但有助于保持记录完整)
                $target->raw_markdown .= "\n\n" . $source->raw_markdown;
                
                // 4. 删除原卷子
                $source->delete();
            }
            
            $target->save();
        });

        $this->selectedIds = [];
        $this->selectPaper($target->id);
        
        Notification::make()
            ->title('卷子合并成功')
            ->success()
            ->send();
    }

    public function selectAllVisible(): void
    {
        $this->selectedIds = $this->papers()->pluck('id')->toArray();
    }

    public function clearSelection(): void
    {
        $this->selectedIds = [];
    }

    public function gradeOptions(): array
    {
        return collect(range(1, 12))->mapWithKeys(fn ($grade) => [$grade => $grade . '年级'])->toArray();
    }

    public function termOptions(): array
    {
        return [
            '上册' => '上册',
            '下册' => '下册',
            '上学期' => '上学期',
            '下学期' => '下学期',
        ];
    }

    public function sourceTypeOptions(): array
    {
        return [
            '期中' => '期中卷',
            '期末' => '期末卷',
            '单元卷' => '单元卷',
            '专项卷' => '专项卷',
            '教材' => '教材',
            '其他' => '其他',
        ];
    }

    public function textbookOptions(): array
    {
        $query = Textbook::query();
        
        // 核心联动：基于 series_id 进行物理过滤
        $seriesId = $this->form['textbook_series_id'] ?? null;
        if ($seriesId) {
            $query->where('series_id', $seriesId);
        }

        return $query->orderBy('id')
            ->get(['id', 'official_title'])
            ->mapWithKeys(function ($textbook) {
                $title = $textbook->official_title ?: '未命名教材';
                return [$textbook->id => $title];
            })
            ->toArray();
    }

    public function seriesOptions(): array
    {
        return \App\Models\TextbookSeries::query()
            ->orderBy('sort_order')
            ->pluck('name', 'id')
            ->toArray();
    }

    public function catalogOptions(?int $textbookId = null): array
    {
        $textbookId ??= $this->form['textbook_id'] ?? null;
        
        if (empty($textbookId)) {
            return [];
        }

        $nodes = TextbookCatalog::query()
            ->where('textbook_id', $textbookId)
            ->orderBy('sort_order')
            ->get(['id', 'title', 'parent_id']);

        $grouped = $nodes->groupBy('parent_id');
        $walk = function ($parent, int $depth) use (&$walk, $grouped): array {
            $items = [];
            foreach ($grouped->get($parent, collect()) as $node) {
                $title = $node->title ?: '未命名目录';
                $indent = str_repeat('—', $depth);
                $items[$node->id] = trim($indent . ' ' . $title);
                $items += $walk($node->id, $depth + 1);
            }
            return $items;
        };

        return $walk(null, 0);
    }

    public function catalogCoverageSummary(): array
    {
        $textbookId = $this->selectedTextbookId();
        if (!$textbookId) {
            return [];
        }

        $nodes = TextbookCatalog::query()
            ->where('textbook_id', $textbookId)
            ->whereDoesntHave('children')
            ->get(['id']);

        $coverage = [];
        SourcePaper::query()
            ->where('textbook_id', $textbookId)
            ->get(['meta'])
            ->each(function ($paper) use (&$coverage) {
                $ids = $paper->meta['catalog_node_ids'] ?? (isset($paper->meta['catalog_node_id']) ? [$paper->meta['catalog_node_id']] : []);
                foreach ((array) $ids as $id) {
                    if ($id) {
                        $coverage[$id] = ($coverage[$id] ?? 0) + 1;
                    }
                }
            });

        $linked = count($coverage);
        $missing = max(0, $nodes->count() - count($coverage));

        return [
            'total' => $nodes->count(),
            'linked' => $linked,
            'missing' => $missing,
        ];
    }

    public function missingCatalogNodes(): array
    {
        $textbookId = $this->selectedTextbookId();
        if (!$textbookId) {
            return [];
        }

        $nodes = TextbookCatalog::query()
            ->where('textbook_id', $textbookId)
            ->whereDoesntHave('children')
            ->orderBy('sort_order')
            ->get(['id', 'title']);

        $coverage = [];
        SourcePaper::query()
            ->where('textbook_id', $textbookId)
            ->get(['meta'])
            ->each(function ($paper) use (&$coverage) {
                $ids = $paper->meta['catalog_node_ids'] ?? (isset($paper->meta['catalog_node_id']) ? [$paper->meta['catalog_node_id']] : []);
                foreach ((array) $ids as $id) {
                    if ($id) {
                        $coverage[$id] = ($coverage[$id] ?? 0) + 1;
                    }
                }
            });

        $missing = [];
        foreach ($nodes as $node) {
            if (!isset($coverage[$node->id])) {
                $missing[] = [
                    'id' => $node->id,
                    'title' => $node->title,
                ];
            }
        }

        return array_slice($missing, 0, 8);
    }

    public function catalogTitlesForPaper(SourcePaper|array $paper): array
    {
        $meta = $paper instanceof SourcePaper ? ($paper->meta ?? []) : ($paper['meta'] ?? []);
        $textbookId = $paper instanceof SourcePaper ? $paper->textbook_id : ($paper['textbook_id'] ?? null);
        if (empty($textbookId)) {
            return [];
        }

        $ids = $this->normalizeCatalogNodeIds($meta['catalog_node_ids'] ?? ($meta['catalog_node_id'] ?? []));
        if (empty($ids)) {
            return [];
        }

        $missing = array_values(array_diff($ids, array_keys($this->catalogNodeCache)));
        if (!empty($missing)) {
            TextbookCatalog::query()
                ->whereIn('id', $missing)
                ->get(['id', 'title'])
                ->each(function ($node) {
                    $this->catalogNodeCache[(int) $node->id] = $node->title ?: ('目录 #' . $node->id);
                });
        }

        return array_values(array_filter(array_map(fn ($id) => $this->catalogNodeCache[$id] ?? null, $ids)));
    }

    public function textbookSuggestions(): array
    {
        $paper = $this->selectedPaper();
        if (!$paper) {
            return [];
        }

        $title = (string) ($paper->title ?? $paper->full_title ?? '');
        $context = mb_strtolower($title);
        $parsed = $this->parseImportFilename();
        $grade = $paper->grade ? (int) $paper->grade : ($parsed['grade'] ?? null);
        $semester = $this->termToSemester($paper->term) ?? $this->termToSemester($parsed['term'] ?? null);
        $seriesHint = $paper->textbook_series ?: ($parsed['series'] ?? null);
        $subjectHint = $parsed['subject'] ?? null;

        return $this->inferenceService()->getTextbookSuggestions($paper, $parsed);
    }

    public function catalogSuggestions(): array
    {
        $paper = $this->selectedPaper();
        $textbookId = $paper?->textbook_id ?? ($this->form['textbook_id'] ?? null);
        if (!$paper || !$textbookId) {
            return [];
        }

        $needle = trim((string) ($paper->chapter ?? $paper->title ?? ''));
        if ($needle === '') {
            return [];
        }

        $nodes = TextbookCatalog::query()
            ->where('textbook_id', $textbookId)
            ->orderBy('sort_order')
            ->get(['id', 'title']);

        $matches = [];
        foreach ($nodes as $node) {
            $title = (string) $node->title;
            if ($title !== '' && str_contains($needle, $title)) {
                $matches[] = ['id' => $node->id, 'title' => $title];
            }
        }

        return array_slice($matches, 0, 5);
    }

    public function applyTextbookSuggestion(int $textbookId): void
    {
        $textbook = Textbook::query()->with('series')->find($textbookId);
        if (!$textbook) {
            return;
        }

        $this->form['textbook_id'] = $textbook->id;
        $this->form['textbook_series'] = $textbook->series?->name ?? $this->form['textbook_series'];
    }

    public function applyCatalogSuggestion(int $catalogId): void
    {
        $ids = $this->form['catalog_node_ids'] ?? [];
        if (!in_array($catalogId, $ids)) {
            $ids[] = $catalogId;
            $this->form['catalog_node_ids'] = $ids;
            $this->savePaper();
        }
    }

    public function candidateCountFor(SourcePaper $paper): int
    {
        return (int) ($paper->candidates_count ?? 0);
    }

    public function checkCompleteness(): array
    {
        $paper = $this->selectedPaper();
        if (!$paper) {
            return [];
        }

        $candidates = $paper->candidates()
            ->where('is_question_candidate', true)
            ->get();

        $service = app(QuestionCandidateToQuestionService::class);
        $total = $candidates->count();
        $invalid = 0;
        $issueStats = [];

        foreach ($candidates as $candidate) {
            $errors = $service->validateCandidate($candidate);
            if (!empty($errors)) {
                $invalid++;
                foreach ($errors as $error) {
                    $issueStats[$error] = ($issueStats[$error] ?? 0) + 1;
                }
            }
        }

        return [
            'total' => $total,
            'valid' => $total - $invalid,
            'invalid' => $invalid,
            'issues' => $issueStats,
            'is_ready' => $total > 0 && $invalid === 0,
        ];
    }

    private function explodeTags(string $tags): array
    {
        return array_values(array_filter(array_map('trim', explode(',', $tags))));
    }

    private function termToSemester(?string $term): ?int
    {
        return $this->inferenceService()->termToSemester($term);
    }

    private function buildBundleKey(SourcePaper $paper): string
    {
        $import = $this->importRecord();
        $base = $import?->file_name
            ? pathinfo($import->file_name, PATHINFO_FILENAME)
            : ($paper->title ?: $paper->full_title ?: '卷子');

        $grade = $paper->grade ? $this->gradeToLabel((int) $paper->grade) : null;
        $term = $paper->term ? $paper->term : null;
        $sourceType = $paper->source_type ?: null;

        $parts = array_filter([$grade, $term, $sourceType, $base]);
        return implode('·', $parts);
    }

    private function gradeToLabel(int $grade): string
    {
        $map = [
            7 => '七年级',
            8 => '八年级',
            9 => '九年级',
            10 => '高一',
            11 => '高二',
            12 => '高三',
        ];

        return $map[$grade] ?? $grade . '年级';
    }

    private function selectedTextbookId(): ?int
    {
        return $this->form['textbook_id'] ?: $this->batch['textbook_id'];
    }

    private function applyFilenameDefaults(): void
    {
        if (!$this->importId || empty($this->filenameParsed)) {
            return;
        }

        $parsed = $this->filenameParsed;
        $resolvedTextbook = $this->inferenceService()->resolveTextbookFromFilename($parsed);
        $papers = SourcePaper::query()
            ->whereHas('candidates', fn ($q) => $q->where('import_id', $this->importId))
            ->get();

        foreach ($papers as $paper) {
            $meta = $paper->meta ?? [];
            $hasNumericGrade = is_numeric((string) $paper->grade);
            $hasAllDefaults = !empty($paper->textbook_id)
                && !empty($paper->textbook_series)
                && $hasNumericGrade
                && !empty($paper->term);
            if (!empty($meta['filename_defaults_applied']) && $hasAllDefaults) {
                continue;
            }

            $updates = [];

            if (empty($paper->textbook_series) && !empty($parsed['series'])) {
                $formal = $this->inferenceService()->resolveSeries($parsed['series']);
                $updates['textbook_series_id'] = $formal ? $formal->id : null;
                $updates['textbook_series'] = $formal ? $formal->name : $parsed['series'];
            }
            
            if (!empty($parsed['grade']) && empty($paper->grade)) {
                $updates['grade'] = $parsed['grade'];
            }
            
            if (!empty($parsed['term']) && empty($paper->term)) {
                $updates['term'] = $parsed['term'];
            }

            // 在应用文件名默认值时，也触发一次教材重定向
            if (empty($paper->textbook_id)) {
                $best = $this->inferenceService()->findBestTextbook([
                    'series_id' => $updates['textbook_series_id'] ?? null,
                    'textbook_series' => $updates['textbook_series'] ?? $paper->textbook_series,
                    'grade' => $updates['grade'] ?? $paper->grade,
                    'term' => $updates['term'] ?? $paper->term,
                ]);
                if ($best) {
                    $updates['textbook_id'] = $best->id;
                }
            }
            
            if (empty($meta['source_name']) && !empty($parsed['name'])) {
                $meta['source_name'] = $parsed['name'];
            }

            if (!empty($updates)) {
                $meta['filename_defaults_applied'] = true;
                $updates['meta'] = $meta;
                $paper->update($updates);
            }
        }
    }

    private function parseImportFilename(): array
    {
        $import = $this->importRecord();
        return $import?->parseFilename() ?? [];
    }

    private function normalizeGradeForForm($grade, array $parsed): ?int
    {
        if (is_numeric($grade)) {
            return (int) $grade;
        }

        $map = [
            '七年级' => 7,
            '八年级' => 8,
            '九年级' => 9,
            '高一' => 10,
            '高二' => 11,
            '高三' => 12,
        ];

        $grade = is_string($grade) ? trim($grade) : '';
        if ($grade !== '' && isset($map[$grade])) {
            return $map[$grade];
        }

        $parsedGrade = $parsed['grade'] ?? null;
        return is_numeric($parsedGrade) ? (int) $parsedGrade : null;
    }

    private function normalizeCatalogNodeIds($value): array
    {
        $ids = is_array($value) ? $value : [$value];
        $ids = array_values(array_unique(array_map('intval', array_filter($ids))));
        return $ids;
    }

    private function filterCatalogNodeIdsForTextbook(?int $textbookId, array $ids): array
    {
        $ids = $this->normalizeCatalogNodeIds($ids);
        if (empty($textbookId) || empty($ids)) {
            return $ids;
        }

        $validIds = TextbookCatalog::query()
            ->where('textbook_id', $textbookId)
            ->whereIn('id', $ids)
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->toArray();

        return $validIds;
    }

    private function applyBatchToIds(array $ids): void
    {
        $targetIds = array_values(array_filter(array_unique(array_map('intval', $ids))));
        if (empty($targetIds)) {
            return;
        }

        $updates = array_filter([
            'edition' => $this->batch['edition'] ?? null,
            'grade' => $this->batch['grade'] ?? null,
            'term' => $this->batch['term'] ?? null,
            'chapter' => $this->batch['chapter'] ?? null,
            'source_type' => $this->batch['source_type'] ?? null,
            'source_year' => $this->batch['source_year'] ?? null,
            'textbook_id' => $this->batch['textbook_id'] ?? null,
            'series_id' => $this->batch['textbook_series_id'] ?? null,
        ], fn ($value) => $value !== null && $value !== '');

        foreach (SourcePaper::query()->whereIn('id', $targetIds)->get() as $paper) {
            $meta = $paper->meta ?? [];

            if (!empty($this->batch['source_name'])) {
                $meta['source_name'] = $this->batch['source_name'];
            }
            if (!empty($this->batch['source_page'])) {
                $meta['source_page'] = $this->batch['source_page'];
            }
            if (!empty($this->batch['tags'])) {
                $meta['tags'] = $this->explodeTags($this->batch['tags']);
            }
            if (!empty($this->batch['bundle_key'])) {
                $meta['bundle_key'] = $this->batch['bundle_key'];
            }
            if (!empty($this->batch['expected_count'])) {
                $meta['expected_count'] = $this->batch['expected_count'];
            }
            if (!empty($this->batch['catalog_node_ids'])) {
                $catalogNodeIds = $this->normalizeCatalogNodeIds($this->batch['catalog_node_ids']);
                $catalogNodeIds = $this->filterCatalogNodeIdsForTextbook($updates['textbook_id'] ?? $paper->textbook_id, $catalogNodeIds);
                $meta['catalog_node_ids'] = $catalogNodeIds;
                $meta['catalog_node_id'] = $catalogNodeIds[0] ?? null;
            }

            $paper->update(array_merge($updates, ['meta' => $meta]));
        }
    }

    private function isBatchEmpty(): bool
    {
        $fields = [
            'edition', 'grade', 'term', 'chapter', 'source_type',
            'source_year', 'textbook_id', 'textbook_series_id',
            'source_name', 'source_page', 'tags', 'bundle_key',
            'expected_count',
        ];

        foreach ($fields as $field) {
            if (!empty($this->batch[$field])) {
                return false;
            }
        }

        return empty($this->batch['catalog_node_ids']);
    }
}
