# -*- coding: utf-8 -*-
"""Curated search scopes for agent research.

The lists are intentionally conservative and human-maintained. They are not a
ranking endorsement; they give agents a safer first pass over recognizable
public-information sources before widening the search.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, replace

from guanlan.source_packs import domains_for_scope


@dataclass(frozen=True)
class SearchScope:
    id: str
    name: str
    description: str
    source_type: str
    trust_level: int
    domains: tuple[str, ...]

    def to_dict(self) -> dict:
        return asdict(self)


SEARCH_SCOPES: dict[str, SearchScope] = {
    "party_central": SearchScope(
        id="party_central",
        name="党央媒与中央重点媒体",
        description="适合政策、宏观叙事、官方表述和权威报道的第一轮检索。",
        source_type="党央媒",
        trust_level=5,
        domains=(
            "people.com.cn",
            "xinhuanet.com",
            "cctv.com",
            "cntv.cn",
            "qstheory.cn",
            "12371.cn",
            "gmw.cn",
            "ce.cn",
            "cnr.cn",
            "cri.cn",
            "chinanews.com.cn",
            "china.com.cn",
            "chinadaily.com.cn",
        ),
    ),
    "gov": SearchScope(
        id="gov",
        name="政府与部委网站",
        description="适合政策原文、法规、统计、产业主管部门通知和官方公告。",
        source_type="政府/部委",
        trust_level=5,
        domains=(
            "gov.cn",
            "mfa.gov.cn",
            "ndrc.gov.cn",
            "miit.gov.cn",
            "mofcom.gov.cn",
            "mof.gov.cn",
            "stats.gov.cn",
            "samr.gov.cn",
            "nhc.gov.cn",
            "nmpa.gov.cn",
            "moj.gov.cn",
            "court.gov.cn",
            "npc.gov.cn",
            "wenshu.court.gov.cn",
            "pbc.gov.cn",
            "csrc.gov.cn",
            "cac.gov.cn",
        ),
    ),
    "local_official": SearchScope(
        id="local_official",
        name="核心地方官媒",
        description="适合观察地方政策、区域产业、城市治理和地方舆论表述。",
        source_type="地方官媒",
        trust_level=4,
        domains=(
            "bjd.com.cn",
            "jfdaily.com",
            "thepaper.cn",
            "eastday.com",
            "southcn.com",
            "ycwb.com",
            "xhby.net",
            "dzwww.com",
            "dahe.cn",
            "rednet.cn",
            "cnhubei.com",
            "cqnews.net",
            "newssc.org",
            "yunnan.cn",
            "fjnews.com",
            "hinews.cn",
            "gxnews.com.cn",
            "hebnews.cn",
        ),
    ),
    "business": SearchScope(
        id="business",
        name="商业与产业媒体",
        description="适合商业模式、公司动态、一级市场、产业观察和创投报道。",
        source_type="商业/产业媒体",
        trust_level=3,
        domains=(
            "36kr.com",
            "huxiu.com",
            "cyzone.cn",
            "iyiou.com",
            "ebrun.com",
            "tmtpost.com",
            "geekpark.net",
            "pingwest.com",
            "leiphone.com",
            "donews.com",
            "jiemian.com",
            "yicai.com",
        ),
    ),
    "ecommerce": SearchScope(
        id="ecommerce",
        name="电商与零售垂类",
        description="适合电商平台、跨境、品牌零售、新消费和产业带研究。",
        source_type="电商/零售垂类",
        trust_level=3,
        domains=(
            "ebrun.com",
            "iyiou.com",
            "donews.com",
            "jiemian.com",
            "linkshop.com",
            "ccfa.org.cn",
            "100ec.cn",
            "chuhai-club.com",
            "cifnews.com",
            "egainnews.com",
        ),
    ),
    "tech_dev": SearchScope(
        id="tech_dev",
        name="科技与开发者社区",
        description="适合技术趋势、产品反馈、开发者讨论和工程实践。",
        source_type="科技/开发者社区",
        trust_level=3,
        domains=(
            "v2ex.com",
            "juejin.cn",
            "segmentfault.com",
            "csdn.net",
            "cnblogs.com",
            "oschina.net",
            "infoq.cn",
            "51cto.com",
            "sspai.com",
            "ithome.com",
        ),
    ),
    "wps_office": SearchScope(
        id="wps_office",
        name="金山办公/WPS 与 AI Office",
        description="适合 WPS、WPS AI、WPS 365、AI 办公、PPT/文档/表格/PDF、协同办公、SaaS、信创、安全和办公 Agent 选题线索。",
        source_type="办公软件/AI Office/SaaS",
        trust_level=3,
        domains=(
            "wps.cn",
            "365.wps.cn",
            "bbs.wps.cn",
            "security.wps.cn",
            "kdocs.cn",
            "wps.com",
            "kingsoftoffice.com",
            "ir.kingsoft.com",
            "microsoft.com",
            "support.microsoft.com",
            "techcommunity.microsoft.com",
            "office.com",
            "microsoft365.com",
            "workspace.google.com",
            "notion.so",
            "coda.io",
            "canva.com",
            "gamma.app",
            "beautiful.ai",
            "tome.app",
            "prezi.com",
            "pitch.com",
            "feishu.cn",
            "larkoffice.com",
            "yuque.com",
            "shimo.im",
            "processon.com",
            "jianguoyun.com",
            "ithome.com",
            "sspai.com",
            "36kr.com",
            "huxiu.com",
            "geekpark.net",
            "leiphone.com",
            "jiqizhixin.com",
            "qbitai.com",
            "aiera.com.cn",
            "infoq.cn",
            "v2ex.com",
            "juejin.cn",
            "producthunt.com",
            "g2.com",
            "capterra.com",
            "reddit.com",
            "news.ycombinator.com",
        ),
    ),
    "cybersecurity": SearchScope(
        id="cybersecurity",
        name="网络安全/CVE/反诈",
        description="适合 CVE、漏洞、补丁、厂商安全公告、网络诈骗和钓鱼风险核验。",
        source_type="网络安全/漏洞/反诈",
        trust_level=5,
        domains=(
            "nvd.nist.gov",
            "cisa.gov",
            "mitre.org",
            "cve.org",
            "cnvd.org.cn",
            "cnnvd.org.cn",
            "cert.org.cn",
            "cert.org",
            "openssl.org",
            "msrc.microsoft.com",
            "security.googleblog.com",
            "krebsonsecurity.com",
        ),
    ),
    "academic": SearchScope(
        id="academic",
        name="学术与论文检索",
        description="适合论文投稿、学术会议、EI/SCI/Scopus 检索、出版商规范和高校认定口径。",
        source_type="学术/论文检索",
        trust_level=4,
        domains=(
            "elsevier.com",
            "engineeringvillage.com",
            "sciencedirect.com",
            "ieee.org",
            "acm.org",
            "springer.com",
            "webofscience.com",
            "clarivate.com",
            "cnki.net",
            "wanfangdata.com.cn",
            "cqvip.com",
            "xueshu.baidu.com",
        ),
    ),
    "science": SearchScope(
        id="science",
        name="科学机构与科研新闻",
        description="适合科学发现核验、航天/天文、生物医学研究新闻和论文/机构原文追踪。",
        source_type="科学机构/科研新闻",
        trust_level=5,
        domains=(
            "nasa.gov",
            "esa.int",
            "nature.com",
            "science.org",
            "sciencemag.org",
            "arxiv.org",
            "pnas.org",
            "noirlab.edu",
            "stsci.edu",
            "nih.gov",
        ),
    ),
    "university": SearchScope(
        id="university",
        name="高校招生与院系官网",
        description="适合研究生招生、导师名单、院系介绍、招生目录、培养方案和高校官方通知。",
        source_type="高校/院系官网",
        trust_level=5,
        domains=(
            "edu.cn",
            "tsinghua.edu.cn",
            "cs.tsinghua.edu.cn",
            "yz.tsinghua.edu.cn",
            "gradadmission.tsinghua.edu.cn",
            "pku.edu.cn",
            "eecs.pku.edu.cn",
            "zju.edu.cn",
            "fudan.edu.cn",
            "sjtu.edu.cn",
            "ustc.edu.cn",
            "nju.edu.cn",
            "hit.edu.cn",
        ),
    ),
    "test_prep": SearchScope(
        id="test_prep",
        name="考试与培训资料",
        description="适合雅思、托福、公考、职业考试、题库机经、培训合规和官方考试政策。",
        source_type="考试/培训/备考",
        trust_level=3,
        domains=(
            "ielts.org",
            "toefl.cn",
            "ets.org",
            "neea.edu.cn",
            "chinaielts.org",
            "chsi.com.cn",
            "eol.cn",
            "koolearn.com",
            "xdf.cn",
            "zhihu.com",
        ),
    ),
    "finance": SearchScope(
        id="finance",
        name="财经与资本市场",
        description="财经总入口：适合先路由股票、公告、财报、宏观、财经新闻和市场情绪。",
        source_type="财经/资本市场",
        trust_level=4,
        domains=(
            "cninfo.com.cn",
            "sse.com.cn",
            "szse.cn",
            "csrc.gov.cn",
            "stats.gov.cn",
            "pbc.gov.cn",
            "cls.cn",
            "eastmoney.com",
            "stcn.com",
            "cnstock.com",
            "yicai.com",
            "xueqiu.com",
            "sec.gov",
        ),
    ),
    "finance_quote": SearchScope(
        id="finance_quote",
        name="行情、指数与板块",
        description="适合股价、指数、涨跌幅、板块排名、ETF、基金净值等强时效数据入口。",
        source_type="财经/行情数据",
        trust_level=3,
        domains=(
            "quote.eastmoney.com",
            "eastmoney.com",
            "finance.sina.com.cn",
            "xueqiu.com",
            "10jqka.com.cn",
            "csindex.com.cn",
            "sse.com.cn",
            "szse.cn",
            "finance.yahoo.com",
            "nasdaq.com",
            "marketwatch.com",
            "cn.investing.com",
        ),
    ),
    "finance_company": SearchScope(
        id="finance_company",
        name="公司基本面与投资者关系",
        description="适合上市公司财报、年报、投资者关系、主营业务、股本和公司一手资料。",
        source_type="财经/公司一手",
        trust_level=5,
        domains=(
            "cninfo.com.cn",
            "sse.com.cn",
            "szse.cn",
            "hkexnews.hk",
            "sec.gov",
            "nasdaq.com",
            "annualreports.com",
        ),
    ),
    "finance_disclosure": SearchScope(
        id="finance_disclosure",
        name="公告、披露与监管",
        description="适合上市公司公告、交易所披露、监管处罚、减持、质押、停复牌和风险事项。",
        source_type="财经/公告披露",
        trust_level=5,
        domains=(
            "cninfo.com.cn",
            "sse.com.cn",
            "szse.cn",
            "bse.cn",
            "hkexnews.hk",
            "csrc.gov.cn",
            "sec.gov",
            "pbc.gov.cn",
            "safe.gov.cn",
        ),
    ),
    "finance_news": SearchScope(
        id="finance_news",
        name="财经新闻与市场快讯",
        description="适合财经快讯、公司新闻、行业事件、市场报道和多家媒体交叉核验。",
        source_type="财经/新闻报道",
        trust_level=4,
        domains=(
            "cls.cn",
            "stcn.com",
            "cnstock.com",
            "cs.com.cn",
            "yicai.com",
            "caixin.com",
            "21jingji.com",
            "eastmoney.com",
            "wallstreetcn.com",
            "reuters.com",
            "bloomberg.com",
        ),
    ),
    "finance_macro": SearchScope(
        id="finance_macro",
        name="宏观金融与官方数据",
        description="适合 CPI、GDP、社融、利率、汇率、外储、央行政策、统计和国际宏观数据。",
        source_type="财经/宏观数据",
        trust_level=5,
        domains=(
            "stats.gov.cn",
            "pbc.gov.cn",
            "safe.gov.cn",
            "mof.gov.cn",
            "ndrc.gov.cn",
            "csrc.gov.cn",
            "imf.org",
            "worldbank.org",
            "oecd.org",
            "fred.stlouisfed.org",
            "cmegroup.com",
        ),
    ),
    "finance_sentiment": SearchScope(
        id="finance_sentiment",
        name="市场讨论与投资者情绪样本",
        description="适合雪球、股吧、微博、知乎等公开讨论样本；只能作为情绪线索，不作事实主证据。",
        source_type="财经/情绪样本",
        trust_level=2,
        domains=(
            "xueqiu.com",
            "guba.eastmoney.com",
            "eastmoney.com",
            "weibo.com",
            "zhihu.com",
        ),
    ),
    "finance_research": SearchScope(
        id="finance_research",
        name="研报、行业报告与观点",
        description="适合券商/行业报告、市场观点和产业金融分析；需和公告、数据源交叉验证。",
        source_type="财经/研报观点",
        trust_level=3,
        domains=(
            "eastmoney.com",
            "data.eastmoney.com",
            "pdf.dfcfw.com",
            "stock.finance.sina.com.cn",
            "cnstock.com",
            "stcn.com",
            "iresearch.com.cn",
            "199it.com",
            "qianzhan.com",
            "leadleo.com",
        ),
    ),
    "social_web": SearchScope(
        id="social_web",
        name="社交与内容平台公开页",
        description="适合做公开网页层面的口碑、讨论和内容线索搜索。",
        source_type="社交/内容平台",
        trust_level=2,
        domains=(
            "weibo.com",
            "xiaohongshu.com",
            "zhihu.com",
            "bilibili.com",
            "douyin.com",
            "toutiao.com",
            "kuaishou.com",
            "douban.com",
            "tousu.sina.com.cn",
            "12365auto.com",
        ),
    ),
    "career": SearchScope(
        id="career",
        name="招聘/薪资/面经",
        description="适合校招、岗位、薪资、面经、公司口碑、海外求职和人才供需观察。",
        source_type="招聘/职场/薪资",
        trust_level=3,
        domains=(
            "levels.fyi",
            "glassdoor.com",
            "linkedin.com",
            "nowcoder.com",
            "yingjiesheng.com",
            "zhipin.com",
            "liepin.com",
            "lagou.com",
            "maimai.cn",
            "teamblind.com",
            "indeed.com",
        ),
    ),
    "sports": SearchScope(
        id="sports",
        name="体育赛事/转会/数据",
        description="适合赛事比分、球员伤病、转会合同、联赛官方动态和体育媒体报道。",
        source_type="体育/赛事/转会",
        trust_level=3,
        domains=(
            "espn.com",
            "skysports.com",
            "theathletic.com",
            "fifa.com",
            "uefa.com",
            "nba.com",
            "mlb.com",
            "hupu.com",
            "dongqiudi.com",
            "transfermarkt.com",
        ),
    ),
    "weather_disaster": SearchScope(
        id="weather_disaster",
        name="天气/灾害/预警",
        description="适合台风路径、天气预警、地震、灾害应急和官方实时提醒。",
        source_type="天气/灾害/预警",
        trust_level=5,
        domains=(
            "nmc.cn",
            "cma.gov.cn",
            "weather.com.cn",
            "jma.go.jp",
            "noaa.gov",
            "mem.gov.cn",
            "nhc.noaa.gov",
            "usgs.gov",
            "gdacs.org",
        ),
    ),
    "podcast": SearchScope(
        id="podcast",
        name="播客/音频/RSS",
        description="适合播客节目、单集、主播、中文播客推荐和音频 RSS 发现。",
        source_type="播客/音频/RSS",
        trust_level=3,
        domains=(
            "xiaoyuzhoufm.com",
            "podcasts.apple.com",
            "spotify.com",
            "podcastaddict.com",
            "podchaser.com",
            "listennotes.com",
            "google.com",
            "rss.com",
        ),
    ),
    "entertainment": SearchScope(
        id="entertainment",
        name="文娱与内容消费",
        description="适合影视、综艺、音乐、游戏、明星、票房、播放热度和公开口碑的第一轮路由。",
        source_type="文娱/内容平台",
        trust_level=3,
        domains=(
            "douban.com",
            "maoyan.com",
            "piaofang.maoyan.com",
            "lighthouse.alibaba.com",
            "taopiaopiao.com",
            "mtime.com",
            "1905.com",
            "bilibili.com",
            "weibo.com",
            "v.qq.com",
            "iqiyi.com",
            "youku.com",
            "mgtv.com",
            "taptap.cn",
            "gamersky.com",
            "3dmgame.com",
            "ign.com.cn",
            "indienova.com",
            "bangumi.tv",
            "pixiv.net",
            "mangapedia.com",
            "manba.co.jp",
            "comic-walker.com",
            "bookwalker.jp",
        ),
    ),
    "global_entertainment": SearchScope(
        id="global_entertainment",
        name="欧美娱乐与音乐产业",
        description="适合欧美明星、Hollywood、流媒体、奖项、巡演、新歌专辑、榜单和行业报道。",
        source_type="欧美文娱/音乐产业",
        trust_level=3,
        domains=(
            "variety.com",
            "deadline.com",
            "hollywoodreporter.com",
            "billboard.com",
            "rollingstone.com",
            "people.com",
            "ew.com",
            "pitchfork.com",
            "stereogum.com",
            "nme.com",
            "grammy.com",
            "officialcharts.com",
        ),
    ),
    "jp_kr_entertainment": SearchScope(
        id="jp_kr_entertainment",
        name="日韩娱乐与 K-pop/J-pop",
        description="适合日韩娱乐、K-pop、J-pop、韩剧日剧、偶像团体、经纪公司动态和榜单新闻。",
        source_type="日韩文娱/K-pop/J-pop",
        trust_level=3,
        domains=(
            "soompi.com",
            "oricon.co.jp",
            "natalie.mu",
            "entertain.naver.com",
            "koreaherald.com",
            "koreatimes.co.kr",
            "mdpr.jp",
            "realsound.jp",
            "mantan-web.jp",
            "allkpop.com",
            "koreaboo.com",
            "tokyohive.com",
        ),
    ),
    "global_official": SearchScope(
        id="global_official",
        name="英文官方/监管与公共机构",
        description="适合政策、监管、标准、统计、司法和公共卫生等英文一手来源核验。",
        source_type="英文官方/监管",
        trust_level=5,
        domains=(
            "usa.gov",
            "whitehouse.gov",
            "congress.gov",
            "federalregister.gov",
            "sec.gov",
            "ftc.gov",
            "fda.gov",
            "nist.gov",
            "iso.org",
            "iec.ch",
            "cdc.gov",
            "who.int",
            "oecd.org",
            "worldbank.org",
            "europa.eu",
            "ec.europa.eu",
        ),
    ),
    "company_primary": SearchScope(
        id="company_primary",
        name="公司一手资料",
        description="适合产品、价格、发布说明、状态页、投资者关系和官方博客核验。",
        source_type="公司一手资料",
        trust_level=5,
        domains=(
            "openai.com",
            "anthropic.com",
            "googleblog.com",
            "blog.google",
            "microsoft.com",
            "azure.microsoft.com",
            "aws.amazon.com",
            "aboutamazon.com",
            "meta.com",
            "ai.meta.com",
            "nvidia.com",
            "tsmc.com",
            "apple.com",
            "tesla.com",
            "bytedance.com",
            "doubao.com",
            "volcengine.com",
            "stripe.com",
            "shopify.com",
            "luckincoffee.com",
        ),
    ),
    "developer": SearchScope(
        id="developer",
        name="英文开发者与开源",
        description="适合官方文档、代码仓库、issue、release notes、工程实践和开发者反馈。",
        source_type="英文开发者/开源",
        trust_level=4,
        domains=(
            "github.com",
            "docs.github.com",
            "stackoverflow.com",
            "developer.mozilla.org",
            "docs.python.org",
            "nodejs.org",
            "npmjs.com",
            "pypi.org",
            "huggingface.co",
            "pytorch.org",
            "kubernetes.io",
            "docs.docker.com",
            "cloudflare.com",
            "vercel.com",
            "milvus.io",
            "qdrant.tech",
            "weaviate.io",
            "trychroma.com",
        ),
    ),
    "global_news": SearchScope(
        id="global_news",
        name="国际主流新闻",
        description="适合事实报道、时间线和多方说法核验；注意编辑立场和付费墙。",
        source_type="国际主流媒体",
        trust_level=4,
        domains=(
            "reuters.com",
            "apnews.com",
            "bbc.com",
            "cnn.com",
            "nytimes.com",
            "washingtonpost.com",
            "theguardian.com",
            "ft.com",
            "wsj.com",
            "bloomberg.com",
            "cnbc.com",
            "taipeitimes.com",
            "theverge.com",
            "technologyreview.com",
        ),
    ),
    "industry_analysis": SearchScope(
        id="industry_analysis",
        name="英文产业与分析",
        description="适合产业判断、商业分析、投资观点和市场结构线索；需和一手来源交叉验证。",
        source_type="英文产业/分析",
        trust_level=3,
        domains=(
            "gartner.com",
            "forrester.com",
            "mckinsey.com",
            "bain.com",
            "bcg.com",
            "a16z.com",
            "stratechery.com",
            "theinformation.com",
            "semianalysis.com",
            "trendforce.com",
            "tomshardware.com",
            "anandtech.com",
            "ben-evans.com",
            "similarweb.com",
        ),
    ),
    "community_sample": SearchScope(
        id="community_sample",
        name="英文社区样本",
        description="适合公开讨论、开发者/用户样本和争议线索；不能代表总体比例。",
        source_type="英文社区样本",
        trust_level=2,
        domains=(
            "reddit.com",
            "news.ycombinator.com",
            "lobste.rs",
            "medium.com",
            "dev.to",
            "producthunt.com",
            "quora.com",
        ),
    ),
    "market_review": SearchScope(
        id="market_review",
        name="英文评价与消费样本",
        description="适合 SaaS、应用、消费品和服务评价样本；注意商业激励和刷评偏差。",
        source_type="评价/消费样本",
        trust_level=2,
        domains=(
            "g2.com",
            "capterra.com",
            "trustpilot.com",
            "trustradius.com",
            "amazon.com",
            "apps.apple.com",
            "play.google.com",
            "qimai.cn",
            "diandian.com",
            "producthunt.com",
        ),
    ),
}


def _apply_source_pack_domains() -> None:
    """Merge carefully curated source-pack domains into existing scopes."""
    for scope_id, scope in list(SEARCH_SCOPES.items()):
        domains = domains_for_scope(scope_id)
        if not domains:
            continue
        SEARCH_SCOPES[scope_id] = replace(
            scope,
            domains=_unique_domain_tuple((*scope.domains, *domains)),
        )


def _unique_domain_tuple(values: tuple[str, ...]) -> tuple[str, ...]:
    seen: set[str] = set()
    output: list[str] = []
    for value in values:
        domain = str(value or "").strip().lower().removeprefix("www.")
        if domain and domain not in seen:
            seen.add(domain)
            output.append(domain)
    return tuple(output)


_apply_source_pack_domains()


SEARCH_SCOPE_ALIASES = {
    "central": "party_central",
    "party": "party_central",
    "official": "gov",
    "local": "local_official",
    "media": "party_central",
    "vertical": "business",
    "biz": "business",
    "retail": "ecommerce",
    "security": "cybersecurity",
    "cyber": "cybersecurity",
    "cve": "cybersecurity",
    "vulnerability": "cybersecurity",
    "anti-fraud": "cybersecurity",
    "antifraud": "cybersecurity",
    "dev": "tech_dev",
    "tech": "tech_dev",
    "wps": "wps_office",
    "wps-ai": "wps_office",
    "wps_ai": "wps_office",
    "wps365": "wps_office",
    "wps-365": "wps_office",
    "wps_365": "wps_office",
    "kingsoft-office": "wps_office",
    "kingsoft_office": "wps_office",
    "office-ai": "wps_office",
    "ai-office": "wps_office",
    "office_ai": "wps_office",
    "ai_office": "wps_office",
    "productivity": "wps_office",
    "office-suite": "wps_office",
    "ppt": "wps_office",
    "presentation": "wps_office",
    "xinchuang": "wps_office",
    "science": "science",
    "nasa": "science",
    "scholar": "academic",
    "academic": "academic",
    "paper": "academic",
    "university": "university",
    "college": "university",
    "campus": "university",
    "edu": "university",
    "admission": "university",
    "admissions": "university",
    "graduate": "university",
    "faculty": "university",
    "advisor": "university",
    "supervisor": "university",
    "test": "test_prep",
    "exam": "test_prep",
    "ielts": "test_prep",
    "toefl": "test_prep",
    "social": "social_web",
    "career": "career",
    "job": "career",
    "jobs": "career",
    "salary": "career",
    "interview": "career",
    "sports": "sports",
    "sport": "sports",
    "football": "sports",
    "soccer": "sports",
    "nba": "sports",
    "weather": "weather_disaster",
    "disaster": "weather_disaster",
    "typhoon": "weather_disaster",
    "podcast": "podcast",
    "audio": "podcast",
    "global-gov": "global_official",
    "global_gov": "global_official",
    "regulator": "global_official",
    "regulation": "global_official",
    "standards": "global_official",
    "company": "company_primary",
    "vendor": "company_primary",
    "primary": "company_primary",
    "docs": "developer",
    "developer": "developer",
    "opensource": "developer",
    "open-source": "developer",
    "news": "global_news",
    "media_global": "global_news",
    "analysis": "industry_analysis",
    "industry_global": "industry_analysis",
    "community": "community_sample",
    "reddit": "community_sample",
    "reviews": "market_review",
    "review": "market_review",
    "app-review": "market_review",
    "app_review": "market_review",
    "appstore": "market_review",
    "play-store": "market_review",
    "aso": "market_review",
    "saas-review": "market_review",
    "quote": "finance_quote",
    "quotes": "finance_quote",
    "stock": "finance_quote",
    "stocks": "finance_quote",
    "market": "finance_quote",
    "index": "finance_quote",
    "etf": "finance_quote",
    "fund": "finance_quote",
    "finance-quote": "finance_quote",
    "finance_quote": "finance_quote",
    "disclosure": "finance_disclosure",
    "disclosures": "finance_disclosure",
    "filing": "finance_disclosure",
    "filings": "finance_disclosure",
    "announcement": "finance_disclosure",
    "announcements": "finance_disclosure",
    "finance-disclosure": "finance_disclosure",
    "finance_disclosure": "finance_disclosure",
    "finance-company": "finance_company",
    "finance_company": "finance_company",
    "ir": "finance_company",
    "investor-relations": "finance_company",
    "macro": "finance_macro",
    "macro-finance": "finance_macro",
    "finance-macro": "finance_macro",
    "finance_macro": "finance_macro",
    "finance-news": "finance_news",
    "finance_news": "finance_news",
    "market-news": "finance_news",
    "finance-sentiment": "finance_sentiment",
    "finance_sentiment": "finance_sentiment",
    "xueqiu": "finance_sentiment",
    "guba": "finance_sentiment",
    "stock-sentiment": "finance_sentiment",
    "research-report": "finance_research",
    "finance-research": "finance_research",
    "finance_research": "finance_research",
    "brokerage": "finance_research",
    "entertainment": "entertainment",
    "culture": "entertainment",
    "wenyu": "entertainment",
    "yule": "entertainment",
    "movie": "entertainment",
    "film": "entertainment",
    "tv": "entertainment",
    "music": "entertainment",
    "game": "entertainment",
    "gaming": "entertainment",
    "acg": "entertainment",
    "anime": "entertainment",
    "manga": "entertainment",
    "comic": "entertainment",
    "light-novel": "entertainment",
    "light_novel": "entertainment",
    "bangumi": "entertainment",
    "pixiv": "entertainment",
    "douban": "entertainment",
    "maoyan": "entertainment",
    "global-entertainment": "global_entertainment",
    "global_entertainment": "global_entertainment",
    "western-entertainment": "global_entertainment",
    "western_entertainment": "global_entertainment",
    "hollywood": "global_entertainment",
    "billboard": "global_entertainment",
    "celebrity": "global_entertainment",
    "pop": "global_entertainment",
    "jp-kr-entertainment": "jp_kr_entertainment",
    "jp_kr_entertainment": "jp_kr_entertainment",
    "japan-entertainment": "jp_kr_entertainment",
    "korea-entertainment": "jp_kr_entertainment",
    "kpop": "jp_kr_entertainment",
    "k-pop": "jp_kr_entertainment",
    "jpop": "jp_kr_entertainment",
    "j-pop": "jp_kr_entertainment",
    "kdrama": "jp_kr_entertainment",
    "k-drama": "jp_kr_entertainment",
    "oricon": "jp_kr_entertainment",
    "soompi": "jp_kr_entertainment",
}


def list_search_scopes() -> dict[str, dict]:
    """Return all curated search scopes as dictionaries."""
    return {key: scope.to_dict() for key, scope in SEARCH_SCOPES.items()}


def resolve_scope(scope_id: str) -> SearchScope:
    """Resolve a scope id or alias."""
    key = (scope_id or "").strip().lower()
    key = SEARCH_SCOPE_ALIASES.get(key, key)
    if key not in SEARCH_SCOPES:
        available = ", ".join(sorted(SEARCH_SCOPES))
        raise ValueError(f"Unknown search scope: {scope_id}. Available: {available}")
    return SEARCH_SCOPES[key]


def scoped_query(query: str, domains: list[str] | tuple[str, ...], max_sites: int = 12) -> str:
    """Build a portable site-restricted query for multiple search engines."""
    selected = [d.strip() for d in domains if d.strip()][:max_sites]
    if not selected:
        return query
    if len(selected) == 1:
        return f"site:{selected[0]} {query}"
    site_expr = " OR ".join(f"site:{domain}" for domain in selected)
    return f"({site_expr}) {query}"


def classify_domain(domain: str, preferred_scope: str | None = None) -> dict:
    """Classify a domain by curated scope membership.

    When a caller already searched within a scope, overlapping domains should
    inherit that requested context. For example, ebrun.com can be both a
    business source and an ecommerce vertical source.
    """
    normalized = (domain or "").lower().removeprefix("www.")
    if not preferred_scope and (normalized == "sec.gov" or normalized.endswith(".sec.gov")):
        scope = SEARCH_SCOPES["global_official"]
        return {
            "source_type": scope.source_type,
            "matched_scope": scope.id,
            "trust_level": scope.trust_level,
        }
    scopes = list(SEARCH_SCOPES.values())
    if preferred_scope:
        preferred = resolve_scope(preferred_scope)
        scopes = [preferred] + [scope for scope in scopes if scope.id != preferred.id]
    else:
        finance_general = SEARCH_SCOPES.get("finance")
        scopes = [scope for scope in scopes if scope.id != "finance"]
        if finance_general:
            scopes.append(finance_general)
    matches: list[tuple[int, int, SearchScope]] = []
    scope_priority = {
        "finance_disclosure": 0,
        "finance_company": 1,
        "finance_quote": 2,
        "finance_macro": 3,
        "finance_news": 4,
        "finance_research": 5,
        "social_web": 6,
        "finance_sentiment": 7,
        "company_primary": 8,
        "developer": 8,
        "tech_dev": 8,
        "community_sample": 8,
        "market_review": 8,
        "business": 8,
        "ecommerce": 8,
        "wps_office": 50,
        "finance": 99,
    }
    for index, scope in enumerate(scopes):
        for candidate in scope.domains:
            if normalized == candidate or normalized.endswith("." + candidate):
                priority = -1 if preferred_scope and scope.id == preferred.id else scope_priority.get(scope.id, index + 20)
                matches.append((len(candidate), -priority, scope))
    if matches:
        _, _, scope = max(matches, key=lambda row: (row[0], row[1]))
        return {
            "source_type": scope.source_type,
            "matched_scope": scope.id,
            "trust_level": scope.trust_level,
        }
    return {
        "source_type": "通用网页",
        "matched_scope": "",
        "trust_level": 1,
    }
