#!/usr/bin/env python3
"""Query compact class surfaces and optional framework lifecycle profiles."""

import argparse
import json
import sys
from collections import Counter
from pathlib import Path

from framework_lookup import (
    DEFAULT_INDEX,
    load_index,
    scaffold_packet,
    validate_index,
)
from lookup import DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize


HELP_EPILOG = """\
查询顺序：
  1. 先按 class 取类摘要或 Framework Profile
     class_lookup.py --class TStringList
     class_lookup.py --class TSBackTesting --format json
  2. 需要成员正文时，再按返回的 qualified_name 调用 lookup.py --name

输出状态：
  status            resolved、not_found 或 ambiguous
  profile_status    resolved 表示有 Framework Profile；not_profiled 表示普通 class
  scaffold_status   生命周期和成员引用是否完整，仅在有 Profile 时输出
  contract_status   回调字段契约是否完整；非 resolved 时不得生成回调字段
  diagnostics       消歧、配置或契约缺口及下一步动作

--check 校验全部 Framework Profile 对 class、成员正文和证据的引用。
使用自定义 --tsv 时默认不加载 Framework Profile；普通 class 仍可正常查询。若该索引
另有 curated Profile，必须同时显式传入 --profiles。

退出码：
  0  查询或校验完成；not_found、ambiguous 通过输出状态表达
  1  function/framework 索引缺失、格式错误、为空或引用校验失败
  2  参数不合法
"""


def empty_profile_index():
    return {"schema_version": 1, "frameworks": []}


def profile_path_for(args):
    if args.profiles:
        return Path(args.profiles)
    if args.tsv:
        return None
    return DEFAULT_INDEX


def class_rows(rows):
    return [row for row in rows if row.get("kind") == "class"]


def filter_rows(rows, scope=None, module=None):
    filtered = rows
    if scope:
        filtered = [
            row for row in filtered if normalize(row.get("scope", "")) == normalize(scope)
        ]
    if module:
        filtered = [
            row
            for row in filtered
            if normalize(row.get("module", "")) == normalize(module)
        ]
    return filtered


def find_classes(rows, name, scope=None, module=None):
    requested = normalize(name)
    matches = [
        row
        for row in class_rows(rows)
        if requested
        in {
            normalize(row.get("name", "")),
            normalize(row.get("qualified_name", "")),
        }
    ]
    return filter_rows(matches, scope, module)


def find_profile(data, row):
    for framework in data.get("frameworks", []):
        if not isinstance(framework, dict):
            continue
        if (
            normalize(framework.get("qualified_name", ""))
            == normalize(row.get("qualified_name", ""))
            and normalize(framework.get("scope", "")) == normalize(row.get("scope", ""))
            and normalize(framework.get("module", ""))
            == normalize(row.get("module", ""))
        ):
            return framework
    return None


def class_members(rows, row):
    owner = normalize(row.get("qualified_name", "") or row.get("name", ""))
    members = [
        candidate
        for candidate in rows
        if candidate.get("kind") != "class"
        and normalize(candidate.get("scope", "")) == normalize(row.get("scope", ""))
        and normalize(candidate.get("module", "")) == normalize(row.get("module", ""))
        and normalize(candidate.get("owner", "")) == owner
    ]
    return sorted(
        members,
        key=lambda item: (
            normalize(item.get("kind", "")),
            normalize(item.get("qualified_name", "") or item.get("name", "")),
            normalize(item.get("signature", "")),
            item.get("anchor", ""),
        ),
    )


def source_id(row):
    return f"{row.get('page', '')}#{row.get('anchor', '')}"


def compact_class(row, profile_available=False):
    return {
        "name": row.get("name", ""),
        "qualified_name": row.get("qualified_name", "") or row.get("name", ""),
        "scope": row.get("scope", ""),
        "module": row.get("module", ""),
        "summary": row.get("summary", ""),
        "profile_available": profile_available,
        "evidence": [source_id(row)],
    }


def compact_member(row):
    return {
        "name": row.get("name", ""),
        "qualified_name": row.get("qualified_name", "") or row.get("name", ""),
        "signature": row.get("signature", ""),
        "kind": row.get("kind", ""),
        "binding": row.get("binding", ""),
        "visibility": row.get("visibility", ""),
        "summary": row.get("summary", ""),
        "evidence": [source_id(row)],
    }


def member_summary(members):
    counts = Counter(member.get("kind", "unknown") or "unknown" for member in members)
    return dict(sorted(counts.items()))


def class_packet(rows, row, profile, config, include_members):
    members = class_members(rows, row)
    packet = {
        "status": "resolved",
        "class": compact_class(row, profile is not None),
        "member_summary": member_summary(members),
        "profile_status": "resolved" if profile is not None else "not_profiled",
        "profile": None,
        "diagnostics": [],
    }
    if include_members:
        packet["members"] = [compact_member(member) for member in members]
    if profile is not None:
        packet["profile"] = scaffold_packet(profile, config)
        packet["diagnostics"] = packet["profile"]["diagnostics"]
    elif config:
        packet["diagnostics"].append(
            {
                "code": "CLASS_NOT_PROFILED",
                "severity": "error",
                "message": "Configuration contracts require a curated framework profile.",
                "next_action": "Query class members directly or add an evidence-backed profile.",
            }
        )
    return packet


def ambiguous_packet(matches, profiles):
    return {
        "status": "ambiguous",
        "candidates": [
            compact_class(row, find_profile(profiles, row) is not None) for row in matches
        ],
        "diagnostics": [
            {
                "code": "AMBIGUOUS_CLASS",
                "severity": "error",
                "message": "Multiple classes share this name across scopes or modules.",
                "next_action": "Pass --scope and --module, or use a qualified class name.",
            }
        ],
    }


def print_class_text(packet):
    if packet["status"] != "resolved":
        print(f"status: {packet['status']}")
        for candidate in packet.get("candidates", []):
            print(
                f"- {candidate['qualified_name']} "
                f"({candidate['scope']}/{candidate['module']})"
            )
        for diagnostic in packet.get("diagnostics", []):
            print(f"- {diagnostic['code']}: {diagnostic['message']}")
        return

    item = packet["class"]
    print(f"class: {item['qualified_name']}")
    print(f"scope/module: {item['scope']}/{item['module']}")
    print(f"summary: {item['summary']}")
    print(
        "members: "
        + ", ".join(
            f"{kind}={count}" for kind, count in packet["member_summary"].items()
        )
    )
    print(f"profile_status: {packet['profile_status']}")
    profile = packet.get("profile")
    if profile:
        print(f"scaffold_status: {profile['scaffold_status']}")
        print(f"contract_status: {profile['contract_status']}")
        print(
            "lifecycle: "
            + " -> ".join(phase["phase"] for phase in profile["lifecycle"])
        )
        required_hooks = [
            hook["api"] for hook in profile["hooks"] if hook.get("required")
        ]
        print("required_hooks: " + (", ".join(required_hooks) or "none"))
    if packet.get("members"):
        print("member_list:")
        for member in packet["members"]:
            print(
                f"- {member['qualified_name']}\t{member['kind']}\t"
                f"{member['signature']}\t{member['evidence'][0]}"
            )
    if packet["diagnostics"]:
        print("diagnostics:")
        for diagnostic in packet["diagnostics"]:
            print(f"- {diagnostic['code']}: {diagnostic['message']}")


def print_list_text(packet):
    print(f"# {packet['count']} indexed classes")
    print("qualified_name\tscope\tmodule\tprofile\tsummary")
    for item in packet["classes"]:
        print(
            "\t".join(
                [
                    item["qualified_name"],
                    item["scope"],
                    item["module"],
                    "framework" if item["profile_available"] else "generic",
                    item["summary"],
                ]
            )
        )


def main(argv=None):
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")
    parser = argparse.ArgumentParser(
        description="查询 TSL class 摘要及其证据化的 Framework Profile。",
        epilog=HELP_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        allow_abbrev=False,
    )
    action = parser.add_mutually_exclusive_group(required=True)
    action.add_argument("--class", dest="class_name", help="精确 class 名或完全限定名")
    action.add_argument("--list", action="store_true", help="列出索引中的 class")
    action.add_argument("--check", action="store_true", help="校验全部 Framework Profile 引用")
    parser.add_argument("--scope", help="按 class scope 过滤")
    parser.add_argument("--module", help="按 class module 过滤")
    parser.add_argument(
        "--config",
        action="append",
        default=[],
        metavar="KEY=VALUE",
        help="传入原始框架配置，可重复",
    )
    parser.add_argument(
        "--include-members", action="store_true", help="在类摘要中包含紧凑成员清单"
    )
    parser.add_argument(
        "--format", choices=("text", "json"), default="text", help="输出格式"
    )
    parser.add_argument("--profiles", metavar="PATH", help="Framework Profile 索引路径")
    parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv 路径")
    args = parser.parse_args(argv)

    if args.config and not args.class_name:
        parser.error("--config requires --class")
    if args.include_members and not args.class_name:
        parser.error("--include-members requires --class")

    tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
    profile_path = profile_path_for(args)
    try:
        profiles = (
            load_index(profile_path) if profile_path else empty_profile_index()
        )
        rows = load_rows(tsv_path)
    except (OSError, UnicodeError, ValueError) as error:
        print(f"ERROR: {error}", file=sys.stderr)
        return 1
    if not rows:
        print(f"ERROR: {tsv_path} has no entries", file=sys.stderr)
        return 1

    errors = validate_index(profiles, rows, codegen_root_for_tsv(tsv_path))
    if errors:
        for error in errors:
            print(f"ERROR: {error}", file=sys.stderr)
        return 1

    indexed_classes = filter_rows(class_rows(rows), args.scope, args.module)
    if args.check:
        print(
            f"OK: {len(class_rows(rows))} classes indexed; "
            f"{len(profiles['frameworks'])} framework profile(s) validated"
        )
        return 0

    if args.list:
        items = [
            compact_class(row, find_profile(profiles, row) is not None)
            for row in sorted(
                indexed_classes,
                key=lambda item: (
                    normalize(item.get("qualified_name", "") or item.get("name", "")),
                    normalize(item.get("scope", "")),
                    normalize(item.get("module", "")),
                ),
            )
        ]
        packet = {"status": "resolved", "count": len(items), "classes": items}
        if args.format == "json":
            print(json.dumps(packet, ensure_ascii=False, indent=2))
        else:
            print_list_text(packet)
        return 0

    matches = find_classes(rows, args.class_name, args.scope, args.module)
    if not matches:
        packet = {
            "status": "not_found",
            "class": args.class_name,
            "diagnostics": [
                {
                    "code": "CLASS_NOT_FOUND",
                    "severity": "error",
                    "message": f"No indexed class named {args.class_name!r}.",
                    "next_action": "Use --list or query an API directly with lookup.py.",
                }
            ],
        }
    elif len(matches) > 1:
        packet = ambiguous_packet(matches, profiles)
    else:
        config = {}
        config_errors = []
        for value in args.config:
            key, separator, raw_value = value.partition("=")
            if not separator or not key.strip() or not raw_value.strip():
                config_errors.append(f"invalid configuration {value!r}")
            else:
                config[key.strip()] = raw_value.strip()
        row = matches[0]
        packet = class_packet(
            rows,
            row,
            find_profile(profiles, row),
            config,
            args.include_members,
        )
        packet["diagnostics"].extend(
            {
                "code": "INVALID_CONFIGURATION",
                "severity": "error",
                "message": error,
                "next_action": "Use KEY=VALUE syntax with non-empty values.",
            }
            for error in config_errors
        )

    if args.format == "json":
        print(json.dumps(packet, ensure_ascii=False, indent=2))
    else:
        print_class_text(packet)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
