"""高级搜索服务：布尔运算 + 字段限定 + PubMed 查询语法"""

from datetime import datetime

from sqlalchemy import String, and_, case, cast, func, literal_column, not_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.literature import GlobalJournal, GlobalLiterature, GlobalLiteratureTag, GlobalTag, GlobalTagTreeNumber
from app.schemas.literature import cap_pub_date


class AdvancedSearchEngine:
    """PG 高级搜索（ES 就绪后切换 search_service.py）"""

    SEARCH_CACHE_TTL = 300  # 秒（夜间流水线更新，5分钟缓存很安全）

    @staticmethod
    def _search_cache_key(
        query: str, field: str, boolean: str, exact_phrase: bool,
        year_from: int | None, year_to: int | None,
        date_from: str | None, date_to: str | None,
        journal_tiers: list[str] | None, pub_types: list[str] | None,
        study_design: str | None, tag_ids: list[str] | None,
        retracted: str, negative_result: str,
        page: int, page_size: int, sort: str,
    ) -> str:
        """归一化查询参数 → 确定性缓存 key（所有 list 排序后参与哈希）"""
        import hashlib, json
        norm = {
            "q": query.strip().lower(),
            "f": field, "b": boolean, "ep": exact_phrase,
            "yf": year_from, "yt": year_to,
            "df": date_from, "dt": date_to,
            "jt": sorted(journal_tiers) if journal_tiers else [],
            "pt": sorted(pub_types) if pub_types else [],
            "sd": study_design,
            "tid": sorted(tag_ids) if tag_ids else [],
            "r": retracted, "nr": negative_result,
            "p": page, "ps": page_size, "s": sort,
        }
        raw = json.dumps(norm, sort_keys=True, ensure_ascii=False, default=str)
        return f"search:advanced:{hashlib.md5(raw.encode()).hexdigest()}"

    @staticmethod
    async def search(
        db: AsyncSession,
        query: str = "",
        field: str = "all",
        boolean: str = "and",
        exact_phrase: bool = False,
        year_from: int | None = None,
        year_to: int | None = None,
        date_from: str | None = None,    # YYYY-MM-DD
        date_to: str | None = None,      # YYYY-MM-DD
        journal_tiers: list[str] | None = None,
        pub_types: list[str] | None = None,
        study_design: str | None = None,  # primary category: interventional / observational / synthesis / etc
        tag_ids: list[str] | None = None,
        retracted: str = "",             # "yes", "no", "only"
        negative_result: str = "",       # "yes", "no", "only"
        page: int = 1,
        page_size: int = 20,
        sort: str = "date",
        cursor_date: str | None = None,  # keyset 游标：上一页最后一条的 pub_date
        cursor_id: str | None = None,    # keyset 游标：上一页最后一条的 id（UUID）
    ) -> dict:
        """执行高级搜索"""
        conditions = []

        # 60s 缓存（仅 page 模式，cursor 游标不重复）
        use_cursor = (cursor_date is not None and cursor_id is not None and sort == "date")
        _cache = None
        _search_cache_key = None
        if not use_cursor:
            from app.core.cache import cache as _cache
            _search_cache_key = AdvancedSearchEngine._search_cache_key(
                query, field, boolean, exact_phrase,
                year_from, year_to, date_from, date_to,
                journal_tiers, pub_types, study_design, tag_ids,
                retracted, negative_result, page, page_size, sort,
            )
            cached = await _cache.get(_search_cache_key)
            if cached is not None:
                return cached

        # ─── PubMed 语法检测与解析 ───
        _pubmed_parsed = None
        _is_flat_text = True  # 是否走传统 tsvector + ILIKE 路径
        if query.strip():
            from app.services.pubmed_query_parser import is_pubmed_syntax, parse_pubmed_query

            if is_pubmed_syntax(query):
                pp = parse_pubmed_query(query)
                _pubmed_parsed = pp

                # 只要解析器产出了有效结构，就走 PubMed 路径
                has_pubmed_terms = bool(
                    pp.title_terms or pp.abstract_terms or pp.tiab_terms
                    or pp.author_terms or pp.journal_terms
                    or pp.mesh_terms or pp.majr_terms
                    or pp.pub_types or pp.doi_terms or pp.pmid_terms
                    or pp.plain_terms or pp.has_not
                    or pp.year_from or pp.year_to
                )
                if has_pubmed_terms:
                    _is_flat_text = False
                    conditions = await AdvancedSearchEngine._pubmed_conditions(
                        db, pp, conditions,
                    )

        # ─── 传统搜索路径（纯文本 / PubMed 退化） ───
        if _is_flat_text and query.strip():
            # 中文搜索：自动匹配 GlobalTag.name_zh → 注入 tag_ids，跳过 ILIKE
            import re as _re
            _CHINESE_RE = _re.compile(r'[一-鿿]')
            if _CHINESE_RE.search(query):
                tag_matches = (await db.execute(
                    select(GlobalTag.id).where(GlobalTag.name_zh.ilike(f'%{query.strip()}%'))
                )).scalars().all()
                if tag_matches:
                    existing = set(tag_ids or [])
                    tag_ids = list(existing | {str(t.id) for t in tag_matches})
                    query = ""  # ILIKE 对中文标题/摘要无效，跳过
            if query.strip():
                terms = [t.strip() for t in query.split() if t.strip()]
            # 单数字词：优先 PMID 精确匹配（unique index 5ms 返回）
            # 不是 PMID 时才回退到 ILIKE 兜底（DOI 片段等），不做 tsquery 避免 seq scan
            numeric_terms = [t for t in terms if t.isdigit() and len(t) <= 15]
            text_terms = [t for t in terms if not (t.isdigit() and len(t) <= 15)]
            for t in numeric_terms:
                p = int(t)
                exists = (await db.execute(
                    select(GlobalLiterature.id).where(GlobalLiterature.pmid == p).limit(1)
                )).scalar_one_or_none()
                if exists:
                    conditions.append(GlobalLiterature.pmid == p)
                else:
                    # 不是 PMID → ILIKE 搜索（DOI 等），不做 tsquery 避免 seq scan
                    p_like = t if exact_phrase else f"%{t}%"
                    conditions.append(or_(
                        GlobalLiterature.title.ilike(p_like),
                        GlobalLiterature.doi.ilike(p_like),
                    ))
            if text_terms:
                if boolean == "and":
                    for term in text_terms:
                        conditions.append(AdvancedSearchEngine._field_condition(field, term, exact_phrase))
                else:
                    or_conds = [AdvancedSearchEngine._field_condition(field, t, exact_phrase) for t in text_terms]
                    conditions.append(or_(*or_conds))

        # 年份范围
        if year_from:
            conditions.append(GlobalLiterature.pub_year >= year_from)
        if year_to:
            conditions.append(GlobalLiterature.pub_year <= year_to)

        # 具体日期范围（按天搜索）
        from datetime import date as dt_date
        if date_from:
            try:
                df = dt_date.fromisoformat(date_from)
                conditions.append(GlobalLiterature.pub_date >= df)
            except ValueError:
                raise ValueError(f"Invalid date_from format: {date_from}")
        if date_to:
            try:
                dt = dt_date.fromisoformat(date_to)
                conditions.append(GlobalLiterature.pub_date <= dt)
            except ValueError:
                raise ValueError(f"Invalid date_to format: {date_to}")

        # 期刊等级
        if journal_tiers:
            subq = select(GlobalJournal.issn).where(GlobalJournal.tier.in_(journal_tiers))
            result = await db.execute(subq)
            issns = [r for (r,) in result.all()]
            if issns:
                conditions.append(GlobalLiterature.journal_issn.in_(issns))

        # 标签筛选（含子标签递归）
        if tag_ids:
            import uuid as _uuid
            tag_uuids = [_uuid.UUID(tid) if isinstance(tid, str) else tid for tid in tag_ids]
            all_tags = (await db.execute(select(GlobalTag).where(GlobalTag.id.in_(tag_uuids)))).scalars().all()
            all_tag_ids = set(t.id for t in all_tags if t)
            # Batch child tag lookup (1 query instead of N)
            paths = [t.path + "::" for t in all_tags if t]
            if paths:
                child_conds = [GlobalTag.path.like(f"{p}%") for p in paths]
                children = (await db.execute(select(GlobalTag.id).where(or_(*child_conds)))).scalars().all()
                all_tag_ids.update(children)
            uids = list(all_tag_ids)
            subq = select(func.distinct(GlobalLiteratureTag.literature_id)).where(GlobalLiteratureTag.tag_id.in_(uids))
            conditions.append(GlobalLiterature.id.in_(subq))

        # 发表类型（PG JSONB contains）
        if pub_types:
            from sqlalchemy.dialects.postgresql import JSONB
            type_conds = [GlobalLiterature.pub_types.cast(JSONB).contains([pt]) for pt in pub_types]
            conditions.append(or_(*type_conds))

        # 研究设计分类过滤（PG JSON path access）
        if study_design:
            conditions.append(
                GlobalLiterature.study_design['primary'].astext == study_design
            )

        # 撤稿过滤
        if retracted == "yes":
            conditions.append(GlobalLiterature.retracted == True)
        elif retracted == "no":
            conditions.append(GlobalLiterature.retracted == False)
        elif retracted == "only":
            conditions.append(GlobalLiterature.retracted == True)

        # 阴性结果过滤
        if negative_result == "yes":
            conditions.append(GlobalLiterature.is_negative_result == True)
        elif negative_result == "no":
            conditions.append(GlobalLiterature.is_negative_result == False)
        elif negative_result == "only":
            conditions.append(GlobalLiterature.is_negative_result == True)

        # 构建查询
        q = select(GlobalLiterature)
        if conditions:
            q = q.where(and_(*conditions))

        # 排序（PubMed 查询时跳过 ts_rank，避免语法标签噪音）
        _relevance_query = query
        if _pubmed_parsed and sort == "relevance":
            # 用纯文本词做相关性排序，去掉 [field] 标签
            plain_parts = [t.text for t in _pubmed_parsed.plain_terms]
            plain_parts += [t.text for t in _pubmed_parsed.title_terms]
            plain_parts += [t.text for t in _pubmed_parsed.tiab_terms]
            _relevance_query = " ".join(plain_parts) if plain_parts else ""
        if sort == "date":
            q = q.order_by(GlobalLiterature.pub_date.desc().nullslast())
        elif sort == "cited":
            q = q.order_by(GlobalLiterature.cited_by_count.desc().nullslast())
        elif sort == "best_match" and _relevance_query.strip():
            tsq = func.plainto_tsquery("english", _relevance_query)
            q = q.order_by(AdvancedSearchEngine._best_match_order(tsq))
        elif sort == "relevance" and _relevance_query.strip():
            tsq = func.plainto_tsquery("english", _relevance_query)
            rank = func.ts_rank(GlobalLiterature.search_tsv, tsq)
            q = q.order_by(rank.desc())
        else:
            q = q.order_by(GlobalLiterature.pub_date.desc().nullslast())

        # keyset 游标分页（仅 date 排序支持，page 参数被忽略，不做 COUNT）
        use_keyset = (cursor_date is not None and cursor_id is not None and sort == "date")
        if use_keyset:
            from datetime import date as dt_date
            try:
                cursor_dt = dt_date.fromisoformat(cursor_date)
            except (ValueError, TypeError):
                use_keyset = False
            if use_keyset:
                import uuid as _cuuid
                try:
                    cursor_uuid = _cuuid.UUID(cursor_id)
                    conditions.append(
                        or_(
                            GlobalLiterature.pub_date < cursor_dt,
                            and_(GlobalLiterature.pub_date == cursor_dt, GlobalLiterature.id < cursor_uuid),
                        )
                    )
                except (ValueError, AttributeError):
                    use_keyset = False

        # 重新构建查询（keyset 条件可能已追加）
        q = select(GlobalLiterature)
        if conditions:
            q = q.where(and_(*conditions))
        if sort == "date":
            q = q.order_by(GlobalLiterature.pub_date.desc().nullslast())
        elif sort == "cited":
            q = q.order_by(GlobalLiterature.cited_by_count.desc().nullslast())
        elif sort == "best_match" and _relevance_query.strip():
            tsq = func.plainto_tsquery("english", _relevance_query)
            q = q.order_by(AdvancedSearchEngine._best_match_order(tsq))
        elif sort == "relevance" and _relevance_query.strip():
            tsq = func.plainto_tsquery("english", _relevance_query)
            rank = func.ts_rank(GlobalLiterature.search_tsv, tsq)
            q = q.order_by(rank.desc())
        else:
            q = q.order_by(GlobalLiterature.pub_date.desc().nullslast())

        # 分页
        has_more = False
        if use_keyset:
            # cursor 模式：取 page_size+1 条判断是否有下一页，不做 COUNT
            result = await db.execute(q.limit(page_size + 1))
            items = result.scalars().all()
            has_more = len(items) > page_size
            items = items[:page_size]
            total = 0
        else:
            # 总数
            count_q = select(func.count()).select_from(
                select(literal_column("1"))
                .select_from(GlobalLiterature)
                .where(and_(*conditions) if conditions else True)
                .subquery()
            )
            total = (await db.execute(count_q)).scalar() or 0
            offset = (page - 1) * page_size

            # ── date 排序优化 ──
            # PG 对常见词（如 "cancer" >80% 匹配率）会选 Parallel Seq Scan（~22s）。
            # 嵌入子查询先取最近 N 条 ID（走 ix_gl_pub_date 索引），迫使 PG
            # 使用索引而非全表扫描。
            # 条件：仅当无可搜索查询且非历史日期筛选时启用，避免丢失早期文献
            _has_meaningful_query = bool(query.strip())
            _has_historical_filter = year_from is not None and year_from < datetime.now().year - 5
            if sort == "date" and conditions and not _has_meaningful_query and not _has_historical_filter:
                recent_n = max(offset + page_size * 5, 2000)
                recent_subq = select(GlobalLiterature.id).where(
                    GlobalLiterature.pub_date.isnot(None)
                ).order_by(GlobalLiterature.pub_date.desc()).limit(recent_n)
                data_q = select(GlobalLiterature).where(
                    GlobalLiterature.id.in_(recent_subq),
                    and_(*conditions)
                ).order_by(GlobalLiterature.pub_date.desc().nullslast())
            else:
                data_q = q

            result = await db.execute(data_q.offset(offset).limit(page_size))
            items = result.scalars().all()
            has_more = (offset + page_size) < total and len(items) == page_size

        from app.services.tag_loader import load_tags_for_literature
        tm = await load_tags_for_literature(db, [str(lit.id) for lit in items])

        # Batch load journal tiers + canonical names
        tier_map = {}
        name_map = {}
        if items:
            issns = list(set(lit.journal_issn for lit in items if lit.journal_issn))
            if issns:
                jr = await db.execute(
                    select(GlobalJournal.issn, GlobalJournal.tier, GlobalJournal.name).where(GlobalJournal.issn.in_(issns))
                )
                for issn_, tier_, name_ in jr.all():
                    tier_map[issn_] = tier_
                    name_map[issn_] = name_

        results = []
        for lit in items:
            authors = lit.authors or []
            results.append({
                "id": str(lit.id), "pmid": lit.pmid, "title": lit.title,
                "first_author": authors[0].get("family", "") if authors else "",
                "journal": name_map.get(lit.journal_issn) or lit.journal, "pub_date": cap_pub_date(lit.pub_date),
                "article_date": lit.article_date.isoformat() if lit.article_date else None,
                "pub_year": lit.pub_year, "tags": tm.get(str(lit.id), []),
                "abstract": lit.abstract[:300] if lit.abstract else None,
                "doi": lit.doi,
                "pmc_id": lit.pmc_id,
                "is_oa": lit.is_oa,
                "cited_by_count": lit.cited_by_count,
                "created_at": lit.created_at.isoformat() if lit.created_at else None,
                "updated_at": lit.updated_at.isoformat() if lit.updated_at else None,
                "journal_issn": lit.journal_issn,
                "journal_tier": tier_map.get(lit.journal_issn),
                "pub_types": lit.pub_types,
                "affiliation": authors[0].get("affiliation", "") if authors else "",
            })

        result = {"items": results, "total": total, "page": page, "page_size": page_size, "has_more": has_more}

        if _search_cache_key is not None:
            await _cache.set(_search_cache_key, result, ttl=AdvancedSearchEngine.SEARCH_CACHE_TTL)

        return result

    @staticmethod
    async def _pubmed_conditions(
        db: AsyncSession,
        pp,
        existing_conditions: list,
    ) -> list:
        """将解析后的 PubMed 查询转换为 SQLAlchemy 条件列表。"""
        from sqlalchemy.dialects.postgresql import JSONB

        conditions = list(existing_conditions)

        # 1. 字段级搜索 [TI] [AB] [TIAB] [AU] [TA]
        field_map = {
            "title": pp.title_terms,
            "abstract": pp.abstract_terms,
            "all": pp.tiab_terms,
            "author": pp.author_terms,
            "journal": pp.journal_terms,
        }
        for fld, terms in field_map.items():
            if not terms:
                continue
            field_conds = []
            for term in terms:
                cond = AdvancedSearchEngine._field_condition(fld, term.text, term.exact)
                if term.is_not:
                    cond = not_(cond)
                field_conds.append(cond)
            # 同 field 内多个 term 按 boolean_operator 组合
            if pp.boolean_operator == "or":
                conditions.append(or_(*field_conds))
            else:
                conditions.extend(field_conds)  # and_() 由外部统一组合

        # 2. 纯文本词（无字段标签）
        if pp.plain_terms:
            if pp.boolean_operator == "or":
                plain_conds = []
                for term in pp.plain_terms:
                    cond = AdvancedSearchEngine._field_condition("all", term.text, term.exact)
                    if term.is_not:
                        cond = not_(cond)
                    plain_conds.append(cond)
                if plain_conds:
                    conditions.append(or_(*plain_conds))
            else:
                for term in pp.plain_terms:
                    cond = AdvancedSearchEngine._field_condition("all", term.text, term.exact)
                    if term.is_not:
                        cond = not_(cond)
                    conditions.append(cond)

        # 3. [MH] → tree_number 展开（逐 term 独立 subq，AND 组合）
        if pp.mesh_terms:
            if pp.boolean_operator == "or":
                conditions = await AdvancedSearchEngine._expand_mesh_tag_ids(
                    db, pp.mesh_terms, conditions)
            else:
                for m in pp.mesh_terms:
                    conditions = await AdvancedSearchEngine._expand_mesh_tag_ids(
                        db, [m], conditions)

        # 4. [MAJR] → tree_number 展开 + is_major=True（逐 term 独立 subq）
        if pp.majr_terms:
            if pp.boolean_operator == "or":
                conditions = await AdvancedSearchEngine._expand_mesh_tag_ids(
                    db, pp.majr_terms, conditions, major_only=True)
            else:
                for m in pp.majr_terms:
                    conditions = await AdvancedSearchEngine._expand_mesh_tag_ids(
                        db, [m], conditions, major_only=True)

        # 5. [PT] → pub_types JSONB contains
        if pp.pub_types:
            type_conds = [
                GlobalLiterature.pub_types.cast(JSONB).contains([pt])
                for pt in pp.pub_types
            ]
            conditions.append(or_(*type_conds))

        # 5. [DP] → 年份/日期范围
        if pp.year_from:
            conditions.append(GlobalLiterature.pub_year >= pp.year_from)
        if pp.year_to:
            conditions.append(GlobalLiterature.pub_year <= pp.year_to)
        if pp.date_from:
            from datetime import date as _dt_date
            try:
                conditions.append(GlobalLiterature.pub_date >= _dt_date.fromisoformat(pp.date_from))
            except ValueError:
                pass
        if pp.date_to:
            from datetime import date as _dt_date
            try:
                conditions.append(GlobalLiterature.pub_date <= _dt_date.fromisoformat(pp.date_to))
            except ValueError:
                pass

        # 6. [PMID] → 精确匹配
        for pmid_val in pp.pmid_terms:
            conditions.append(GlobalLiterature.pmid == pmid_val)

        # 7. [DOI] → ILIKE
        for doi_term in pp.doi_terms:
            conditions.append(GlobalLiterature.doi.ilike(f"%{doi_term}%"))

        return conditions

    @staticmethod
    def _field_condition(field: str, term: str, exact: bool) -> callable:
        if field == "title":
            if exact:
                return GlobalLiterature.title.ilike(term if exact else f"%{term}%")
            # 只用 tsquery（GIN 索引），不掺 ILIKE——OR 会让 PG 放弃 GIN 索引走 seq scan
            return GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term))
        elif field == "abstract":
            if exact:
                return GlobalLiterature.abstract.ilike(term if exact else f"%{term}%")
            return GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term))
        elif field == "author":
            return GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term))
        elif field == "journal":
            return or_(
                GlobalLiterature.journal.ilike(f"%{term}%"),
                GlobalLiterature.journal_iso.ilike(f"%{term}%"),
            )
        elif field == "affiliation":
            return GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term))
        else:  # "all" default
            like_val = term if exact else f"%{term}%"
            if exact:
                return or_(
                    GlobalLiterature.title.ilike(like_val),
                    cast(GlobalLiterature.pmid, String).ilike(f"%{term}%"),
                    GlobalLiterature.doi.ilike(like_val),
                )
            # 含 "/" → 跳过 tsquery
            if "/" in term:
                if term.startswith("10."):
                    return or_(
                        GlobalLiterature.doi == term,
                        GlobalLiterature.doi.ilike(like_val),
                    )
                return or_(
                    GlobalLiterature.title.ilike(like_val),
                    cast(GlobalLiterature.pmid, String).ilike(f"%{term}%"),
                    GlobalLiterature.doi.ilike(like_val),
                )
            # 只用 tsquery（GIN 索引），不掺 ILIKE——OR 会让 PG 放弃 GIN 索引走 seq scan
            return GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term))

    @staticmethod
    async def _expand_mesh_tag_ids(
        db: AsyncSession,
        mesh_names: list[str],
        conditions: list,
        major_only: bool = False,
    ) -> list:
        """用 GlobalTagTreeNumber 展开 [MH]/[MAJR]

        1. ILIKE 匹配 mesh_names → tag_ids
        2. 用 tree_number 前缀展开子节点（C04.588 → 所有子树号）
        3. [MAJR] 额外 AND is_major=True
        """
        import uuid as _uuid
        mesh_conds = [GlobalTag.name_en.ilike(f"%{m}%") for m in mesh_names]
        tag_rows = (await db.execute(
            select(GlobalTag.id).where(or_(*mesh_conds))
        )).all()
        if not tag_rows:
            return conditions

        mesh_tag_ids: set[_uuid.UUID] = set(tid for (tid,) in tag_rows)

        # tree_number 前缀展开：取匹配 tag 的所有 tree_number，查子节点
        tns = (await db.execute(
            select(GlobalTagTreeNumber.tree_number).where(
                GlobalTagTreeNumber.tag_id.in_(list(mesh_tag_ids))
            ).distinct()
        )).scalars().all()

        if tns:
            child_conds = [GlobalTagTreeNumber.tree_number.like(f"{tn}%") for tn in tns]
            children = (await db.execute(
                select(GlobalTagTreeNumber.tag_id).where(or_(*child_conds))
            )).scalars().all()
            mesh_tag_ids.update(children)

        uids = list(mesh_tag_ids)
        if major_only:
            subq = select(func.distinct(GlobalLiteratureTag.literature_id)).where(
                GlobalLiteratureTag.tag_id.in_(uids),
                GlobalLiteratureTag.is_major == True,
            )
        else:
            subq = select(func.distinct(GlobalLiteratureTag.literature_id)).where(
                GlobalLiteratureTag.tag_id.in_(uids),
            )
        conditions.append(GlobalLiterature.id.in_(subq))
        return conditions

    @staticmethod
    def _best_match_order(tsq):
        """构建 best_match 排序表达式：ts_rank * 0.3 + 引用数对数 * 2 + 近期加分"""
        score = (
            func.ts_rank(GlobalLiterature.search_tsv, tsq) * 0.3
            + func.ln(func.coalesce(GlobalLiterature.cited_by_count, 0) + 1) * 2
            + case((GlobalLiterature.pub_year >= 2020, 5), else_=0)
        )
        return score.desc()
