from __future__ import annotations
import asyncio
import ipaddress
import time
from typing import Awaitable, Callable

from .schemas import Finding, ScanResult
from .scoring import apply_rank
from .protocol import constants as C
from .protocol.client import tcp_reachable, resolve_host, send_ssl2_client_hello
from .protocol.probes import (
    probe_tls_version,
    probe_cipher,
    get_peer_certificate_chain,
    alpn_negotiate,
)
from .protocol.kex import detect_named_groups
from .certs.parse import parse_certificate
from .certs.trust import verify_across_platforms
from .certs.pwnedkeys import check_spki
from .certs.revocation import check_ocsp, check_crl
from .hsts import fetch_hsts
from .preload.check import check_preload
from .caa import lookup_caa
from .http import probe_http
from .http3 import probe_http3
from .vulns.heartbleed import probe as probe_heartbleed
from .vulns.ccs import probe as probe_ccs
from .vulns.renego import probe_secure_renegotiation, probe_fallback_scsv
from .vulns.analysis import analyze as analyze_vulns
from .handshake_sim import simulate as simulate_handshakes

ReportProgress = Callable[[str, str, float, str], Awaitable[None]]
ReportFinding = Callable[[Finding], Awaitable[None]]


# Groups: reliability (信頼性), safety (安全性), vulnerabilities (脆弱性), compatibility (互換性)
G_REL = "reliability"
G_SAF = "safety"
G_VLN = "vulnerabilities"
G_CMP = "compatibility"


def parse_target(target: str) -> tuple[str, int, str | None]:
    """Return (host, port, sni). sni=None means the target is an IP literal."""
    s = target.strip()
    port = 443
    host = s
    if s.startswith("["):
        idx = s.rfind("]")
        if idx != -1:
            host = s[1:idx]
            rest = s[idx + 1:]
            if rest.startswith(":"):
                try:
                    port = int(rest[1:])
                except ValueError:
                    port = 443
    elif s.count(":") == 1:
        h, _, p = s.partition(":")
        host = h
        try:
            port = int(p)
        except ValueError:
            port = 443
    else:
        host = s
    host = host.strip()
    sni: str | None = host
    try:
        ipaddress.ip_address(host)
        sni = None
    except ValueError:
        pass
    return host, port, sni


def validate_tls_target(raw: str) -> str | None:
    import re
    s = (raw or "").strip()
    if not s or len(s) > 255:
        return None
    if not re.compile(r"^[A-Za-z0-9._:\[\]\-]{1,255}$").match(s):
        return None
    try:
        host, port, _ = parse_target(s)
    except Exception:
        return None
    if not host or port <= 0 or port > 65535:
        return None
    try:
        ipaddress.ip_address(host)
        return s
    except ValueError:
        pass
    labels = host.split(".")
    if not labels or any(not lbl for lbl in labels):
        return None
    for lbl in labels:
        if len(lbl) > 63 or lbl.startswith("-") or lbl.endswith("-"):
            return None
        if not re.match(r"^[A-Za-z0-9\-]+$", lbl):
            return None
    if len(labels) < 2 and host != "localhost":
        return None
    return s


async def _gather(report: ReportProgress, finds: ReportFinding, result: ScanResult) -> None:
    host = result.host
    port = result.port
    sni = result.data.get("sni")

    async def emit(f: Finding) -> None:
        result.add(f)
        await finds(f)

    # ---- Phase: protocol versions ----
    await report("protocols", "TLS/SSL バージョンを検査中", 0.05, "info")
    versions_to_probe = [C.TLS_1_0, C.TLS_1_1, C.TLS_1_2, C.TLS_1_3]
    version_support: dict[int, bool] = {}
    version_cipher: dict[int, str | None] = {}
    for v in versions_to_probe:
        pr = await probe_tls_version(host, port, sni, v)
        version_support[v] = pr.supported
        version_cipher[v] = pr.negotiated_cipher
    sslv2 = await send_ssl2_client_hello(host, port)
    sslv2_supported = sslv2.connected and sslv2.alert is None and bool(sslv2.raw)
    from .protocol.client import send_client_hello
    from .protocol import wire
    try:
        ssl3 = await send_client_hello(
            host, port,
            record_version=C.SSL_3_0,
            client_hello_version=C.SSL_3_0,
            cipher_suites=[0x002f, 0x0035, 0x000a, 0x0004, 0x0005],
            extensions=(wire.ext_server_name(sni) if sni else b""),
            sni=sni,
        )
        sslv3_supported = (
            ssl3.connected
            and ssl3.server_hello is not None
            and ssl3.server_hello.alert is None
            and ssl3.server_hello.server_version == C.SSL_3_0
        )
    except Exception:
        sslv3_supported = False
    version_support[C.SSL_2_0] = sslv2_supported
    version_support[C.SSL_3_0] = sslv3_supported
    versions_supported = {v for v, ok in version_support.items() if ok}
    # Emit versions in chronological order — SSL 2.0, SSL 3.0, then TLS 1.0+.
    # Reads left-to-right in the results table the way humans describe it.
    ordered_versions = [C.SSL_2_0, C.SSL_3_0, C.TLS_1_0, C.TLS_1_1, C.TLS_1_2, C.TLS_1_3]
    result.data["versions"] = {
        C.PROTOCOL_NAMES.get(v, f"0x{v:04x}"): version_support.get(v, False)
        for v in ordered_versions
    }

    # SSL 2/3 → vulnerability findings; also record them as safety findings.
    if sslv2_supported:
        await emit(Finding("SSL/TLS Version", "SSL 2.0 supported", "SSLv2 は完全に破綻しています (DROWN)", "serious", 10, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "SSL 2.0 disabled", "", "good", 0, group=G_SAF))
    if sslv3_supported:
        await emit(Finding("SSL/TLS Version", "SSL 3.0 supported", "POODLE 攻撃が可能", "serious", 10, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "SSL 3.0 disabled", "", "good", 0, group=G_SAF))
    if version_support.get(C.TLS_1_0):
        await emit(Finding("SSL/TLS Version", "TLS 1.0 supported", "RFC 8996 で廃止済み", "notgood", 4, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "TLS 1.0 disabled", "", "good", 0, group=G_SAF))
    if version_support.get(C.TLS_1_1):
        await emit(Finding("SSL/TLS Version", "TLS 1.1 supported", "RFC 8996 で廃止済み", "notgood", 2, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "TLS 1.1 disabled", "", "good", 0, group=G_SAF))
    if version_support.get(C.TLS_1_2):
        await emit(Finding("SSL/TLS Version", "TLS 1.2 supported", "", "good", 0, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "TLS 1.2 not supported", "推奨プロトコルが有効化されていません", "notgood", 3, group=G_SAF))
    if version_support.get(C.TLS_1_3):
        await emit(Finding("SSL/TLS Version", "TLS 1.3 supported", "", "good", 0, group=G_SAF))
    else:
        await emit(Finding("SSL/TLS Version", "TLS 1.3 not supported", "最新プロトコルが有効化されていません", "notgood", 3, group=G_SAF))
    if not version_support.get(C.TLS_1_2) and not version_support.get(C.TLS_1_3):
        await emit(Finding("SSL/TLS Version", "No modern TLS (1.2/1.3) supported", "", "serious", 10, group=G_SAF))

    # ---- Phase: cipher enumeration ----
    await report("ciphers", "暗号スイートを列挙中", 0.20, "info")
    accepted_per_version: dict[int, list[int]] = {}
    probe_ciphers_per_version = {
        C.TLS_1_3: [0x1301, 0x1302, 0x1303, 0x1304, 0x1305],
        C.TLS_1_2: [
            0xc02b, 0xc02c, 0xc02f, 0xc030, 0xcca8, 0xcca9,
            0xc013, 0xc014, 0xc009, 0xc00a, 0xc027, 0xc028,
            0x009c, 0x009d, 0x009e, 0x009f, 0xccaa,
            0x002f, 0x0035, 0x003c, 0x003d,
            0x000a, 0xc012, 0x0016,
            0x0004, 0x0005, 0xc011, 0xc007,
            0x0001, 0x0002, 0x003b,
            0x0008, 0x0014,
            0x0018, 0x0034,
        ],
        C.TLS_1_1: [0xc013, 0xc014, 0xc009, 0xc00a, 0x002f, 0x0035, 0x000a, 0x0004, 0x0005],
        C.TLS_1_0: [0xc013, 0xc014, 0xc009, 0xc00a, 0x002f, 0x0035, 0x000a, 0x0004, 0x0005],
    }
    for v in (C.TLS_1_3, C.TLS_1_2, C.TLS_1_1, C.TLS_1_0):
        if not version_support.get(v):
            continue
        accepted: list[int] = []
        for cid in probe_ciphers_per_version[v]:
            name = C.CIPHER_SUITES.get(cid, f"0x{cid:04x}")
            try:
                probe = await probe_cipher(host, port, sni, v, cid, name)
            except Exception:
                continue
            if probe.supported:
                accepted.append(cid)
        accepted_per_version[v] = accepted
    result.data["ciphers"] = {
        C.PROTOCOL_NAMES.get(v, f"0x{v:04x}"): [C.CIPHER_SUITES.get(c, f"0x{c:04x}") for c in cids]
        for v, cids in accepted_per_version.items()
    }

    any_accepted = {cid for cids in accepted_per_version.values() for cid in cids}
    if any_accepted:
        has_fs = False
        has_aead = False
        has_cbc_modern = False
        has_rsa_kex = False
        for cid in any_accepted:
            name = C.CIPHER_SUITES.get(cid, "")
            if C.cipher_has_fs(name):
                has_fs = True
            if C.cipher_is_aead(name):
                has_aead = True
            if C.cipher_is_cbc(name):
                has_cbc_modern = True
            if name.startswith("TLS_RSA_") and not C.cipher_is_weak(name):
                has_rsa_kex = True
        if has_fs:
            await emit(Finding("Cipher Strength", "Forward secrecy supported", "ECDHE/DHE ciphers 利用可", "good", 0, group=G_SAF))
        else:
            await emit(Finding("Cipher Strength", "No forward secrecy", "ECDHE/DHE が有効になっていない", "bad", 5, group=G_SAF))
        if has_aead:
            await emit(Finding("Cipher Strength", "AEAD ciphers supported", "GCM / ChaCha20-Poly1305 / CCM 利用可", "good", 0, group=G_SAF))
        else:
            await emit(Finding("Cipher Strength", "No AEAD ciphers", "", "notgood", 2, group=G_SAF))
        if has_rsa_kex:
            await emit(Finding("Cipher Strength", "RSA key exchange accepted",
                               "鍵交換に RSA_WITH_* が利用可能 — 前方秘匿性なし", "notgood", 2, group=G_SAF))
        if has_cbc_modern:
            await emit(Finding("Cipher Strength", "CBC ciphers in accepted list",
                               "LUCKY13 など CBC ベース攻撃のリスク", "info", 0, group=G_SAF))

    # ---- Phase: key exchange / groups ----
    await report("kex", "鍵交換グループを検査中", 0.35, "info")
    groups_accepted: list[int] = []
    if version_support.get(C.TLS_1_3):
        try:
            groups_accepted = await detect_named_groups(host, port, sni, C.TLS_1_3)
        except Exception:
            groups_accepted = []
    result.data["named_groups"] = [C.NAMED_GROUPS.get(g, f"0x{g:04x}") for g in groups_accepted]
    if groups_accepted:
        strong = [g for g in groups_accepted if g in (0x001d, 0x0017, 0x0018, 0x0019, 0x001e)]
        pqc = [g for g in groups_accepted if g in C.PQC_GROUPS]
        weak = [g for g in groups_accepted if g in (0x0100,)]
        if strong:
            await emit(Finding("Key Exchange", "Modern named groups supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in strong), "good", 0, group=G_SAF))
        if pqc:
            await emit(Finding("Key Exchange", "Post-quantum key exchange supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in pqc), "good", 0, group=G_SAF))
        else:
            await emit(Finding("Key Exchange", "No post-quantum key exchange",
                               "X25519MLKEM768 等の PQC 鍵交換に対応していません", "info", 0, group=G_SAF))
        if weak:
            await emit(Finding("Key Exchange", "Weak FFDHE group supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in weak), "notgood", 3, group=G_SAF))
    elif version_support.get(C.TLS_1_3):
        await emit(Finding("Key Exchange", "Named groups probe inconclusive", "", "info", 0, group=G_SAF))

    # ---- Phase: certificates ----
    await report("cert", "証明書を取得・解析中", 0.45, "info")
    chain_der = await get_peer_certificate_chain(host, port, sni)
    parsed_chain = []
    leaf_cert = None
    leaf_summary = None
    if chain_der:
        for idx, der in enumerate(chain_der):
            try:
                c, s = parse_certificate(der)
                parsed_chain.append(s)
                if idx == 0:
                    leaf_cert = c
                    leaf_summary = s
            except Exception:
                continue
    result.data["certificate_chain"] = [
        {
            "subject": s.subject,
            "issuer": s.issuer,
            "common_name": s.common_name,
            "sans": s.sans,
            "not_before": s.not_before,
            "not_after": s.not_after,
            "days_until_expiry": s.days_until_expiry,
            "is_expired": s.is_expired,
            "is_self_signed": s.is_self_signed,
            "signature_hash_algorithm": s.signature_hash_algorithm,
            "public_key_algorithm": s.public_key_algorithm,
            "public_key_size_bits": s.public_key_size_bits,
            "public_key_curve": s.public_key_curve,
            "serial": s.serial,
            "has_scts": s.has_scts,
            "sct_count": s.sct_count,
            "sha256_fingerprint": s.sha256_fingerprint,
            "spki_sha256": s.spki_sha256,
            "ocsp_urls": s.ocsp_urls,
            "crl_urls": s.crl_urls,
        }
        for s in parsed_chain
    ]

    if not leaf_summary:
        await emit(Finding("Certificate", "No certificate retrieved", "TLS ハンドシェイクが失敗", "serious", 10, group=G_REL))
    else:
        # Expiry
        if leaf_summary.is_expired:
            await emit(Finding("Certificate Validity", "Certificate expired",
                               f"not_after={leaf_summary.not_after}", "serious", 10, group=G_REL))
        elif leaf_summary.days_until_expiry < 15:
            await emit(Finding("Certificate Validity", "Certificate expires soon",
                               f"残り {leaf_summary.days_until_expiry} 日", "bad", 4, group=G_REL))
        elif leaf_summary.days_until_expiry < 30:
            await emit(Finding("Certificate Validity", "Certificate expiring in <30 days",
                               f"残り {leaf_summary.days_until_expiry} 日", "notgood", 2, group=G_REL))
        else:
            await emit(Finding("Certificate Validity", "Certificate validity OK",
                               f"残り {leaf_summary.days_until_expiry} 日", "good", 0, group=G_REL))

        # Chain length
        if len(parsed_chain) == 1 and not leaf_summary.is_self_signed:
            await emit(Finding("Certificate Chain", "Intermediate certificate not served",
                               "サーバーがチェーンに中間証明書を含めていません", "notgood", 2, group=G_REL))
        elif len(parsed_chain) >= 2:
            await emit(Finding("Certificate Chain", f"Chain length: {len(parsed_chain)} certs",
                               f"leaf → {parsed_chain[-1].subject[:80]}", "good", 0, group=G_REL))

        # Self-signed
        if leaf_summary.is_self_signed:
            await emit(Finding("Certificate Chain", "Self-signed certificate",
                               "CA 署名ではなく自己署名です", "serious", 9, group=G_REL))

        # Signature hash (safety — cryptographic strength)
        sh = (leaf_summary.signature_hash_algorithm or "").lower()
        if sh in ("md5", "md2"):
            await emit(Finding("Signature Algorithm", f"Weak signature hash: {sh.upper()}", "", "serious", 10, group=G_SAF))
        elif sh == "sha1":
            await emit(Finding("Signature Algorithm", "Weak signature hash: SHA1", "", "bad", 5, group=G_SAF))
        elif sh:
            await emit(Finding("Signature Algorithm", f"Signature hash: {sh.upper()}", "", "good", 0, group=G_SAF))

        # Public key strength (safety)
        alg = leaf_summary.public_key_algorithm
        bits = leaf_summary.public_key_size_bits
        if alg == "RSA":
            if bits < 1024:
                await emit(Finding("Key Strength", f"Very weak RSA key ({bits}-bit)", "", "serious", 10, group=G_SAF))
            elif bits < 2048:
                await emit(Finding("Key Strength", f"Weak RSA key ({bits}-bit)", "", "bad", 5, group=G_SAF))
            elif bits < 3072:
                await emit(Finding("Key Strength", f"RSA {bits}-bit", "推奨は 3072-bit 以上", "good", 0, group=G_SAF))
            else:
                await emit(Finding("Key Strength", f"RSA {bits}-bit", "", "good", 0, group=G_SAF))
        elif alg in ("EC", "Ed25519", "Ed448"):
            await emit(Finding("Key Strength",
                               f"{alg} {bits}-bit {leaf_summary.public_key_curve or ''}".strip(),
                               "", "good", 0, group=G_SAF))

        # CT / SCT (reliability — verifiable audit trail)
        if leaf_summary.has_scts:
            await emit(Finding("Transparency", f"Certificate Transparency SCTs present ({leaf_summary.sct_count})",
                               "", "good", 0, group=G_REL))
        else:
            await emit(Finding("Transparency", "No embedded SCTs",
                               "Certificate Transparency ログが埋め込まれていません", "notgood", 1, group=G_REL))

        # Hostname match (reliability — identity verification)
        if sni:
            names = [leaf_summary.common_name] if leaf_summary.common_name else []
            names += leaf_summary.sans
            matched = False
            for n in names:
                if not n:
                    continue
                n = n.lower().strip()
                h = sni.lower().strip()
                if n == h:
                    matched = True
                    break
                if n.startswith("*.") and "." in h and h.split(".", 1)[1] == n[2:]:
                    matched = True
                    break
            if matched:
                await emit(Finding("Hostname", "Hostname matches certificate", "", "good", 0, group=G_REL))
            else:
                await emit(Finding("Hostname", "Hostname does not match certificate",
                                   f"SNI={sni}, CN={leaf_summary.common_name}", "serious", 8, group=G_REL))
        else:
            await emit(Finding("Hostname", "IP literal target: hostname validation skipped",
                               "IP 指定のため SNI/証明書名一致の判定を行っていません", "notgood", 2, group=G_REL))

    # ---- Phase: trust stores ----
    await report("trust", "5 プラットフォームのトラストストアを照合中", 0.55, "info")
    try:
        trust_results = await verify_across_platforms(host, port, sni)
    except Exception as e:
        trust_results = []
        await emit(Finding("Trust Store", "Trust evaluation failed", str(e), "info", 0, group=G_REL))
    result.data["trust"] = [
        {"platform": t.platform, "trusted": t.trusted, "error": t.error} for t in trust_results
    ]
    untrusted = [t for t in trust_results if not t.trusted]
    trusted = [t for t in trust_results if t.trusted]
    if trust_results and not untrusted:
        await emit(Finding("Trust Store", "Trusted on all platforms",
                           ", ".join(t.platform for t in trusted), "good", 0, group=G_REL))
    else:
        for t in untrusted:
            await emit(Finding("Trust Store", f"Not trusted by {t.platform}",
                               t.error or "", "serious", 6, group=G_REL))
        for t in trusted:
            await emit(Finding("Trust Store", f"Trusted by {t.platform}", "", "good", 0, group=G_REL))

    # ---- Phase: revocation ----
    await report("revocation", "証明書失効を確認中", 0.62, "info")
    if leaf_cert and len(chain_der) >= 2:
        try:
            issuer_cert, _ = parse_certificate(chain_der[1])
            ocsp_res = await check_ocsp(leaf_cert, issuer_cert)
            result.data["ocsp"] = {
                "checked": ocsp_res.checked,
                "revoked": ocsp_res.revoked,
                "reason": ocsp_res.reason,
                "source": ocsp_res.source,
                "error": ocsp_res.error,
            }
            if ocsp_res.checked and ocsp_res.revoked:
                await emit(Finding("Revocation", "Certificate revoked (OCSP)",
                                   ocsp_res.reason or "", "serious", 10, group=G_REL))
            elif ocsp_res.checked:
                await emit(Finding("Revocation", "OCSP: certificate is not revoked",
                                   ocsp_res.source or "", "good", 0, group=G_REL))
            elif ocsp_res.error:
                await emit(Finding("Revocation", "OCSP check inconclusive",
                                   ocsp_res.error, "info", 0, group=G_REL))
        except Exception as e:
            result.data["ocsp"] = {"error": str(e)}
    if leaf_cert:
        try:
            crl_res = await check_crl(leaf_cert)
            result.data["crl"] = {
                "checked": crl_res.checked,
                "revoked": crl_res.revoked,
                "source": crl_res.source,
                "error": crl_res.error,
            }
            if crl_res.checked and crl_res.revoked:
                await emit(Finding("Revocation", "Certificate revoked (CRL)",
                                   crl_res.source, "serious", 10, group=G_REL))
        except Exception as e:
            result.data["crl"] = {"error": str(e)}

    # ---- Phase: pwnedkeys ----
    if leaf_summary and leaf_summary.spki_sha256:
        await report("pwnedkeys", "pwnedkeys.com で鍵漏洩を確認中", 0.66, "info")
        try:
            pwned, err = await check_spki(leaf_summary.spki_sha256)
            result.data["pwnedkeys"] = {"pwned": pwned, "error": err}
            if pwned:
                await emit(Finding("Key Exposure", "Private key is publicly known (pwnedkeys)",
                                   "この公開鍵に対応する秘密鍵は既に漏洩しています", "serious", 10, group=G_REL))
            elif err is None:
                await emit(Finding("Key Exposure", "Private key not listed in pwnedkeys", "",
                                   "good", 0, group=G_REL))
        except Exception as e:
            result.data["pwnedkeys"] = {"error": str(e)}

    # ---- Phase: HSTS ----
    await report("hsts", "HSTS 設定を確認中", 0.70, "info")
    try:
        hsts = await fetch_hsts(host, port)
    except Exception as e:
        hsts = None
        result.data["hsts"] = {"error": str(e)}
    if hsts is not None:
        result.data["hsts"] = {
            "present": hsts.present,
            "max_age": hsts.max_age,
            "include_subdomains": hsts.include_subdomains,
            "preload": hsts.preload,
            "raw": hsts.raw,
            "error": hsts.error,
        }
        if hsts.present:
            if hsts.max_age >= 15552000:
                await emit(Finding("HSTS", "HSTS enabled with sufficient max-age",
                                   f"max-age={hsts.max_age}", "good", 0, group=G_REL))
            elif hsts.max_age > 0:
                await emit(Finding("HSTS", "HSTS max-age too short",
                                   f"max-age={hsts.max_age} (推奨 >= 15552000)", "notgood", 2, group=G_REL))
            if hsts.include_subdomains:
                await emit(Finding("HSTS", "HSTS includeSubDomains set", "", "good", 0, group=G_REL))
            else:
                await emit(Finding("HSTS", "HSTS includeSubDomains not set", "", "info", 0, group=G_REL))
        else:
            await emit(Finding("HSTS", "HSTS header missing", "", "notgood", 3, group=G_REL))

    # ---- Phase: HSTS preload ----
    if sni:
        await report("preload", "HSTS preload リストを確認中 (Chrome/Firefox/Edge/IE)", 0.74, "info")
        try:
            preload_results = await check_preload(sni)
        except Exception as e:
            preload_results = []
            result.data["preload_error"] = str(e)
        result.data["preload"] = [
            {"browser": p.browser, "listed": p.listed, "include_subdomains": p.include_subdomains,
             "error": p.source_error}
            for p in preload_results
        ]
        listed_any = [p for p in preload_results if p.listed]
        not_listed = [p for p in preload_results if not p.listed and not p.source_error]
        if listed_any:
            await emit(Finding("HSTS Preload", "Listed in HSTS preload list",
                               ", ".join(p.browser for p in listed_any), "good", 0, group=G_REL))
        if not_listed and not listed_any:
            await emit(Finding("HSTS Preload", "Not on HSTS preload list",
                               ", ".join(p.browser for p in not_listed), "info", 0, group=G_REL))

    # ---- Phase: CAA ----
    if sni:
        await report("caa", "CAA レコードを検査中", 0.77, "info")
        try:
            caa = await lookup_caa(sni)
        except Exception as e:
            caa = None
            result.data["caa"] = {"error": str(e)}
        if caa is not None:
            result.data["caa"] = {
                "effective_host": caa.effective_host,
                "records": caa.records,
                "error": caa.error,
            }
            if caa.records:
                await emit(Finding("CAA", f"CAA records present ({caa.effective_host})",
                                   "; ".join(caa.records), "good", 0, group=G_REL))
            else:
                await emit(Finding("CAA", "No CAA records",
                                   "CAA が設定されていません", "notgood", 1, group=G_REL))

    # ---- Phase: HTTP ----
    await report("http", "HTTP/1/2 を検査中", 0.80, "info")
    try:
        http_info = await probe_http(host, port)
    except Exception as e:
        http_info = None
        result.data["http_error"] = str(e)
    if http_info is not None:
        result.data["http"] = {
            "http1": http_info.http1,
            "http1_status": http_info.http1_status,
            "http2": http_info.http2,
            "http3_via_alt_svc": http_info.http3_via_alt_svc,
            "alt_svc": http_info.alt_svc,
            "server": http_info.server,
            "content_encoding": http_info.content_encoding,
            "compression_enabled": http_info.compression_enabled,
            "valid_http_response": http_info.valid_http_response,
            "error": http_info.error,
        }
        if not http_info.valid_http_response:
            await emit(Finding("HTTP", "No valid HTTP response",
                               http_info.error or "", "bad", 6, group=G_CMP))
        else:
            if http_info.http1:
                await emit(Finding("HTTP", "HTTP/1.1 supported", "", "good", 0, group=G_CMP))
            if http_info.http2:
                await emit(Finding("HTTP", "HTTP/2 supported", "", "good", 0, group=G_CMP))
            else:
                await emit(Finding("HTTP", "HTTP/2 not supported", "", "notgood", 1, group=G_CMP))
            if http_info.compression_enabled:
                await emit(Finding("HTTP Compression",
                                   "HTTP compression enabled (BREACH risk)",
                                   f"content-encoding={http_info.content_encoding}",
                                   "notgood", 2, group=G_VLN))
            else:
                await emit(Finding("HTTP Compression", "HTTP compression disabled",
                                   "BREACH の可能性なし", "good", 0, group=G_VLN))

    # ---- Phase: HTTP/3 ----
    await report("http3", "HTTP/3 (QUIC) を検査中", 0.84, "info")
    try:
        h3 = await probe_http3(host, port, sni)
    except Exception as e:
        h3 = None
        result.data["http3_error"] = str(e)
    if h3 is not None:
        result.data["http3"] = {"supported": h3.supported, "error": h3.error}
        if h3.supported:
            await emit(Finding("HTTP/3", "HTTP/3 supported", "", "good", 0, group=G_CMP))
        else:
            await emit(Finding("HTTP/3", "HTTP/3 not supported", h3.error or "", "info", 0, group=G_CMP))

    # ---- Phase: ALPN ----
    try:
        alpn = await alpn_negotiate(host, port, sni, ["h2", "http/1.1"])
    except Exception:
        alpn = None
    if alpn:
        result.data["alpn"] = alpn
        await emit(Finding("ALPN", f"ALPN negotiated: {alpn}", "", "good", 0, group=G_CMP))

    # ---- Phase: vulnerabilities (passive + active) ----
    await report("vulns", "脆弱性を検査中", 0.88, "info")
    vp = analyze_vulns(versions_supported, accepted_per_version)
    if vp.drown:
        await emit(Finding("DROWN", "DROWN (SSLv2 exposure)", "SSLv2 が有効", "serious", 10, group=G_VLN))
    if vp.poodle_ssl:
        await emit(Finding("POODLE", "POODLE (SSLv3 CBC)", "", "serious", 10, group=G_VLN))
    if vp.beast:
        await emit(Finding("BEAST", "BEAST (TLS 1.0 CBC)", "", "notgood", 3, group=G_VLN))
    if vp.sweet32:
        await emit(Finding("SWEET32", "SWEET32 (3DES)",
                           "3DES が accepted cipher list にあり", "bad", 4, group=G_VLN))
    if vp.rc4:
        await emit(Finding("RC4", "RC4 enabled", "", "serious", 7, group=G_VLN))
    if vp.freak:
        await emit(Finding("FREAK", "FREAK (EXPORT RSA)", "", "serious", 10, group=G_VLN))
    if vp.logjam_export:
        await emit(Finding("LOGJAM", "LOGJAM (EXPORT DHE)", "", "serious", 10, group=G_VLN))
    if vp.null_cipher:
        await emit(Finding("NULL cipher", "NULL cipher enabled", "", "serious", 10, group=G_VLN))
    if vp.anon_cipher:
        await emit(Finding("Anon cipher", "Anonymous cipher enabled", "", "serious", 10, group=G_VLN))
    if vp.lucky13:
        await emit(Finding("LUCKY13", "LUCKY13 (CBC)", "", "notgood", 2, group=G_VLN))

    # If no passive vulns detected across the major ones, surface a positive finding.
    none_of_the_above = not any([
        vp.drown, vp.poodle_ssl, vp.beast, vp.sweet32, vp.rc4,
        vp.freak, vp.logjam_export, vp.null_cipher, vp.anon_cipher,
    ])
    if none_of_the_above and any_accepted:
        await emit(Finding("Cipher Vulnerabilities",
                           "No known cipher-family vulnerabilities",
                           "DROWN / POODLE / FREAK / LOGJAM / RC4 / 3DES / NULL / anon いずれも該当なし",
                           "good", 0, group=G_VLN))

    # Active probes
    try:
        hb = await probe_heartbleed(host, port, sni)
        result.data["heartbleed"] = {
            "vulnerable": hb.vulnerable,
            "heartbeat_extension": hb.heartbeat_extension_advertised,
            "error": hb.error,
        }
        if hb.vulnerable:
            await emit(Finding("Heartbleed", "Heartbleed (CVE-2014-0160)", "", "serious", 10, group=G_VLN))
        elif hb.heartbeat_extension_advertised:
            await emit(Finding("Heartbleed", "Heartbeat extension advertised but not exploitable",
                               "", "notgood", 1, group=G_VLN))
        else:
            await emit(Finding("Heartbleed", "Not vulnerable to Heartbleed", "", "good", 0, group=G_VLN))
    except Exception as e:
        result.data["heartbleed"] = {"error": str(e)}

    try:
        ccs_vuln, ccs_msg = await probe_ccs(host, port, sni)
        result.data["ccs_injection"] = {"vulnerable": ccs_vuln, "detail": ccs_msg}
        if ccs_vuln:
            await emit(Finding("CCS Injection", "CCS Injection (CVE-2014-0224)", "", "serious", 10, group=G_VLN))
        else:
            await emit(Finding("CCS Injection", "Not vulnerable to CCS Injection", "", "good", 0, group=G_VLN))
    except Exception as e:
        result.data["ccs_injection"] = {"error": str(e)}

    try:
        renego_ok, renego_info = await probe_secure_renegotiation(host, port, sni)
        result.data["secure_renegotiation"] = {"supported": renego_ok, "detail": renego_info}
        if renego_ok:
            detail = renego_info or "RFC 5746 renegotiation_info extension acknowledged"
            await emit(Finding("Renegotiation", "Secure renegotiation supported", detail, "good", 0, group=G_VLN))
        else:
            await emit(Finding("Renegotiation", "Secure renegotiation not supported",
                               renego_info or "", "bad", 4, group=G_VLN))
    except Exception as e:
        result.data["secure_renegotiation"] = {"error": str(e)}

    try:
        scsv_ok, scsv_info = await probe_fallback_scsv(host, port, sni)
        result.data["fallback_scsv"] = {"supported": scsv_ok, "detail": scsv_info}
        if scsv_ok:
            detail = scsv_info or "TLS 1.1 ClientHello with SCSV rejected with inappropriate_fallback"
            await emit(Finding("Fallback Protection", "TLS_FALLBACK_SCSV enforced", detail, "good", 0, group=G_VLN))
        else:
            await emit(Finding("Fallback Protection", "TLS_FALLBACK_SCSV not enforced",
                               scsv_info or "", "notgood", 2, group=G_VLN))
    except Exception as e:
        result.data["fallback_scsv"] = {"error": str(e)}

    # ---- Phase: handshake simulation ----
    await report("handshake_sim", "代表的なクライアントとのハンドシェイクを模擬中", 0.95, "info")
    try:
        sim = await simulate_handshakes(host, port, sni)
    except Exception as e:
        sim = []
        result.data["handshake_sim_error"] = str(e)
    result.data["handshake_simulation"] = [
        {
            "client": s.client,
            "connected": s.connected,
            "negotiated_version": s.negotiated_version,
            "negotiated_cipher": s.negotiated_cipher,
            "error": s.error,
        }
        for s in sim
    ]
    if sim:
        ok_count = sum(1 for s in sim if s.connected)
        total = len(sim)
        # Emit per-client findings so the Compatibility tab has granular entries.
        for s in sim:
            if s.connected:
                await emit(Finding("Client Handshake",
                                   f"{s.client}: OK",
                                   f"{s.negotiated_version} / {s.negotiated_cipher}",
                                   "good", 0, group=G_CMP))
            else:
                await emit(Finding("Client Handshake",
                                   f"{s.client}: failed",
                                   s.error or "",
                                   "info", 0, group=G_CMP))
        await emit(Finding("Client Handshake",
                           f"Handshake summary: {ok_count}/{total} clients connected",
                           "", "info", 0, group=G_CMP))


async def run_full_scan(target: str, report: ReportProgress, finds: ReportFinding) -> ScanResult:
    host, port, sni = parse_target(target)
    started = time.time()
    result = ScanResult(target=target, host=host, port=port, started_at=started)
    result.data["sni"] = sni

    # Track the current phase slug so each emitted Finding can be stamped with
    # the step that produced it (used in the UI log as "[step]").
    _step = {"name": ""}
    _orig_report = report
    _orig_finds = finds

    async def report(phase: str, detail: str, progress: float, severity: str = "info") -> None:  # type: ignore[no-redef]
        _step["name"] = phase
        await _orig_report(phase, detail, progress, severity)

    async def finds(f: Finding) -> None:  # type: ignore[no-redef]
        if not f.step:
            f.step = _step["name"]
        await _orig_finds(f)

    await report("init", f"対象: {target} (host={host}, port={port})", 0.01, "info")

    resolved = await resolve_host(host)
    if resolved is None:
        await report("dns", "名前解決に失敗しました", 1.0, "serious")
        result.error = "dns_fail"
        result.finished_at = time.time()
        apply_rank(result)
        result.score = 0.0
        result.rank = "R"
        result.add(Finding("Connectivity", "DNS resolution failed", host, "serious", 10, group=G_REL))
        await finds(result.findings[-1])
        return result
    result.data["resolved_ip"] = resolved

    reachable = await tcp_reachable(host, port)
    if not reachable:
        await report("tcp", f"TCP {host}:{port} に接続できません", 1.0, "serious")
        result.error = "no_tls"
        result.add(Finding("Connectivity", "TCP port unreachable", f"{host}:{port}", "serious", 10, group=G_REL))
        await finds(result.findings[-1])
        result.finished_at = time.time()
        apply_rank(result)
        return result

    try:
        await _gather(report, finds, result)
    except asyncio.CancelledError:
        raise
    except Exception as e:
        result.error = f"{e.__class__.__name__}: {e}"
        result.add(Finding("Engine", "Scan error", str(e), "serious", 5, group=G_REL))
        await finds(result.findings[-1])

    result.finished_at = time.time()
    apply_rank(result)
    await report("done", f"スコア={result.score} ランク={result.rank}", 1.0, "info")
    return result
