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]]


def parse_target(target: str) -> tuple[str, int, str | None]:
    """Return (host, port, sni). sni=None means pass hostname for SNI; None when 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:
        # bare IPv6 without brackets or hostname
        host = s
    host = host.strip()
    sni: str | None = host
    try:
        ipaddress.ip_address(host)
        sni = None
    except ValueError:
        pass
    return host, port, sni


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

    # ---- 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 check via raw
    sslv2 = await send_ssl2_client_hello(host, port)
    sslv2_supported = sslv2.connected and sslv2.alert is None and bool(sslv2.raw)
    # SSLv3 via raw (OpenSSL typically disables SSLv3 even with SECLEVEL=0)
    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}
    result.data["versions"] = {C.PROTOCOL_NAMES.get(v, f"0x{v:04x}"): ok for v, ok in version_support.items()}

    # Findings per version
    if sslv2_supported:
        result.add(Finding("protocol", "SSL 2.0 supported", "SSLv2 は完全に破綻しています", "serious", 10))
        await finds(result.findings[-1])
    if sslv3_supported:
        result.add(Finding("protocol", "SSL 3.0 supported", "POODLE 攻撃が可能", "serious", 10))
        await finds(result.findings[-1])
    if version_support.get(C.TLS_1_0):
        result.add(Finding("protocol", "TLS 1.0 supported", "廃止済みプロトコル (RFC 8996)", "notgood", 4))
        await finds(result.findings[-1])
    if version_support.get(C.TLS_1_1):
        result.add(Finding("protocol", "TLS 1.1 supported", "廃止済みプロトコル (RFC 8996)", "notgood", 2))
        await finds(result.findings[-1])
    if version_support.get(C.TLS_1_2):
        result.add(Finding("protocol", "TLS 1.2 supported", "", "good", 0))
        await finds(result.findings[-1])
    if version_support.get(C.TLS_1_3):
        result.add(Finding("protocol", "TLS 1.3 supported", "", "good", 0))
        await finds(result.findings[-1])
    if not version_support.get(C.TLS_1_2) and not version_support.get(C.TLS_1_3):
        result.add(Finding("protocol", "No modern TLS (1.2/1.3) supported", "", "serious", 10))
        await finds(result.findings[-1])

    # ---- 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()
    }

    # Cipher family findings
    any_accepted = {cid for cids in accepted_per_version.values() for cid in cids}
    if any_accepted:
        has_fs = False
        has_aead = False
        has_weak = 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_weak(name):
                has_weak = True
        if has_fs:
            result.add(Finding("cipher", "Forward secrecy supported", "ECDHE/DHE ciphers 利用可", "good", 0))
            await finds(result.findings[-1])
        else:
            result.add(Finding("cipher", "No forward secrecy", "ECDHE/DHE が有効になっていない", "bad", 5))
            await finds(result.findings[-1])
        if has_aead:
            result.add(Finding("cipher", "AEAD ciphers supported", "GCM/ChaCha20-Poly1305 利用可", "good", 0))
            await finds(result.findings[-1])
        # Specific weak-cipher findings are emitted later under Vulnerabilities (SWEET32/RC4/FREAK/etc.),
        # so we only add an informational summary here to avoid double-penalty.
        if has_weak:
            result.add(Finding("cipher", "Weak cipher family in accepted list",
                               "詳細は Vulnerabilities セクションを参照", "info", 0))
            await finds(result.findings[-1])

    # ---- 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]
        if strong:
            result.add(Finding("kex", "Modern named groups supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in strong), "good", 0))
            await finds(result.findings[-1])
        if pqc:
            result.add(Finding("kex", "Post-quantum key exchange supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in pqc), "good", 0))
            await finds(result.findings[-1])
        weak = [g for g in groups_accepted if g in (0x0100,)]
        if weak:
            result.add(Finding("kex", "Weak FFDHE group supported",
                               ", ".join(C.NAMED_GROUPS.get(g, "") for g in weak), "notgood", 3))
            await finds(result.findings[-1])

    # ---- 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:
        result.add(Finding("cert", "No certificate retrieved", "TLS ハンドシェイクが失敗", "serious", 10))
        await finds(result.findings[-1])
    else:
        # Expiry
        if leaf_summary.is_expired:
            result.add(Finding("cert", "Certificate expired",
                               f"not_after={leaf_summary.not_after}", "serious", 10))
            await finds(result.findings[-1])
        elif leaf_summary.days_until_expiry < 15:
            result.add(Finding("cert", "Certificate expires soon",
                               f"残り {leaf_summary.days_until_expiry} 日", "bad", 4))
            await finds(result.findings[-1])
        elif leaf_summary.days_until_expiry < 30:
            result.add(Finding("cert", "Certificate expiring in <30 days",
                               f"残り {leaf_summary.days_until_expiry} 日", "notgood", 2))
            await finds(result.findings[-1])
        else:
            result.add(Finding("cert", "Certificate validity OK",
                               f"残り {leaf_summary.days_until_expiry} 日", "good", 0))
            await finds(result.findings[-1])

        # Self-signed
        if leaf_summary.is_self_signed:
            result.add(Finding("cert", "Self-signed certificate",
                               "CA 署名ではなく自己署名です", "serious", 9))
            await finds(result.findings[-1])

        # Signature hash
        sh = (leaf_summary.signature_hash_algorithm or "").lower()
        if sh in ("md5", "md2"):
            result.add(Finding("cert", f"Weak signature hash: {sh.upper()}", "", "serious", 10))
            await finds(result.findings[-1])
        elif sh == "sha1":
            result.add(Finding("cert", "Weak signature hash: SHA1", "", "bad", 5))
            await finds(result.findings[-1])
        elif sh:
            result.add(Finding("cert", f"Signature hash: {sh.upper()}", "", "good", 0))
            await finds(result.findings[-1])

        # Public key strength
        alg = leaf_summary.public_key_algorithm
        bits = leaf_summary.public_key_size_bits
        if alg == "RSA":
            if bits < 1024:
                result.add(Finding("cert", f"Very weak RSA key ({bits}-bit)", "", "serious", 10))
                await finds(result.findings[-1])
            elif bits < 2048:
                result.add(Finding("cert", f"Weak RSA key ({bits}-bit)", "", "bad", 5))
                await finds(result.findings[-1])
            elif bits < 3072:
                result.add(Finding("cert", f"RSA {bits}-bit", "推奨は 3072-bit 以上", "good", 0))
                await finds(result.findings[-1])
            else:
                result.add(Finding("cert", f"RSA {bits}-bit", "", "good", 0))
                await finds(result.findings[-1])
        elif alg in ("EC", "Ed25519", "Ed448"):
            result.add(Finding("cert", f"{alg} {bits}-bit {leaf_summary.public_key_curve or ''}".strip(),
                               "", "good", 0))
            await finds(result.findings[-1])

        # CT / SCT
        if leaf_summary.has_scts:
            result.add(Finding("cert", f"Certificate Transparency SCTs present ({leaf_summary.sct_count})",
                               "", "good", 0))
            await finds(result.findings[-1])
        else:
            result.add(Finding("cert", "No embedded SCTs",
                               "Certificate Transparency ログが埋め込まれていません", "notgood", 1))
            await finds(result.findings[-1])

        # Hostname match (for non-IP targets)
        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:
                result.add(Finding("cert", "Hostname matches certificate", "", "good", 0))
                await finds(result.findings[-1])
            else:
                result.add(Finding("cert", "Hostname does not match certificate",
                                   f"SNI={sni}, CN={leaf_summary.common_name}", "serious", 8))
                await finds(result.findings[-1])
        else:
            result.add(Finding("cert", "IP literal target: hostname validation skipped",
                               "IP 指定のため SNI/証明書名一致の判定を行っていません", "notgood", 2))
            await finds(result.findings[-1])

    # ---- 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 = []
        result.add(Finding("trust", "Trust evaluation failed", str(e), "info", 0))
        await finds(result.findings[-1])
    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:
        result.add(Finding("trust", "Trusted on all platforms",
                           ", ".join(t.platform for t in trusted), "good", 0))
        await finds(result.findings[-1])
    else:
        for t in untrusted:
            result.add(Finding("trust", f"Not trusted by {t.platform}",
                               t.error or "", "serious", 6))
            await finds(result.findings[-1])

    # ---- 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:
                result.add(Finding("cert", "Certificate revoked (OCSP)",
                                   ocsp_res.reason or "", "serious", 10))
                await finds(result.findings[-1])
            elif ocsp_res.checked:
                result.add(Finding("cert", "OCSP: certificate is not revoked", "", "good", 0))
                await finds(result.findings[-1])
        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:
                result.add(Finding("cert", "Certificate revoked (CRL)", crl_res.source, "serious", 10))
                await finds(result.findings[-1])
        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:
                result.add(Finding("cert", "Private key is publicly known (pwnedkeys)",
                                   "この公開鍵に対応する秘密鍵は既に漏洩しています", "serious", 10))
                await finds(result.findings[-1])
            elif err is None:
                result.add(Finding("cert", "Private key not listed in pwnedkeys", "", "good", 0))
                await finds(result.findings[-1])
        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:
                result.add(Finding("hsts", "HSTS enabled with sufficient max-age",
                                   f"max-age={hsts.max_age}", "good", 0))
                await finds(result.findings[-1])
            elif hsts.max_age > 0:
                result.add(Finding("hsts", "HSTS max-age too short",
                                   f"max-age={hsts.max_age} (推奨 >= 15552000)", "notgood", 2))
                await finds(result.findings[-1])
            if hsts.include_subdomains:
                result.add(Finding("hsts", "HSTS includeSubDomains set", "", "good", 0))
                await finds(result.findings[-1])
        else:
            result.add(Finding("hsts", "HSTS header missing", "", "notgood", 3))
            await finds(result.findings[-1])

    # ---- 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]
        if listed_any:
            result.add(Finding("hsts", "Listed in HSTS preload list",
                               ", ".join(p.browser for p in listed_any), "good", 0))
            await finds(result.findings[-1])

    # ---- 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:
                result.add(Finding("caa", f"CAA records present ({caa.effective_host})",
                                   "; ".join(caa.records), "good", 0))
                await finds(result.findings[-1])
            else:
                result.add(Finding("caa", "No CAA records", "CAA が設定されていません", "notgood", 1))
                await finds(result.findings[-1])

    # ---- 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:
            result.add(Finding("http", "No valid HTTP response", http_info.error or "", "bad", 6))
            await finds(result.findings[-1])
        else:
            if http_info.http2:
                result.add(Finding("http", "HTTP/2 supported", "", "good", 0))
                await finds(result.findings[-1])
            if http_info.compression_enabled:
                result.add(Finding("http", "HTTP compression enabled",
                                   f"content-encoding={http_info.content_encoding} - BREACH の懸念",
                                   "notgood", 2))
                await finds(result.findings[-1])

    # ---- 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:
            result.add(Finding("http", "HTTP/3 supported", "", "good", 0))
            await finds(result.findings[-1])

    # ---- Phase: ALPN ----
    try:
        alpn = await alpn_negotiate(host, port, sni, ["h2", "http/1.1"])
    except Exception:
        alpn = None
    if alpn:
        result.data["alpn"] = alpn

    # ---- Phase: vulnerabilities ----
    await report("vulns", "脆弱性を検査中", 0.88, "info")
    # Passive analysis based on versions/ciphers
    vp = analyze_vulns(versions_supported, accepted_per_version)
    if vp.drown:
        result.add(Finding("vuln", "DROWN (SSLv2)", "SSLv2 が有効", "serious", 10)); await finds(result.findings[-1])
    if vp.poodle_ssl:
        result.add(Finding("vuln", "POODLE (SSLv3 CBC)", "", "serious", 10)); await finds(result.findings[-1])
    if vp.beast:
        result.add(Finding("vuln", "BEAST (TLS 1.0 CBC)", "クライアント側の緩和策が有効なら実害は限定的", "notgood", 3)); await finds(result.findings[-1])
    if vp.sweet32:
        result.add(Finding("vuln", "SWEET32 (3DES)", "3DES が accepted cipher list にあり", "bad", 4)); await finds(result.findings[-1])
    if vp.rc4:
        result.add(Finding("vuln", "RC4 enabled", "", "serious", 7)); await finds(result.findings[-1])
    if vp.freak:
        result.add(Finding("vuln", "FREAK (EXPORT RSA)", "", "serious", 10)); await finds(result.findings[-1])
    if vp.logjam_export:
        result.add(Finding("vuln", "LOGJAM (EXPORT DHE)", "", "serious", 10)); await finds(result.findings[-1])
    if vp.null_cipher:
        result.add(Finding("vuln", "NULL cipher enabled", "", "serious", 10)); await finds(result.findings[-1])
    if vp.anon_cipher:
        result.add(Finding("vuln", "Anonymous cipher enabled", "", "serious", 10)); await finds(result.findings[-1])
    if vp.lucky13:
        result.add(Finding("vuln", "LUCKY13 (CBC)", "", "notgood", 2)); await finds(result.findings[-1])

    # 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:
            result.add(Finding("vuln", "Heartbleed (CVE-2014-0160)", "", "serious", 10))
            await finds(result.findings[-1])
        elif hb.heartbeat_extension_advertised:
            result.add(Finding("vuln", "Heartbeat extension advertised but not exploitable",
                               "", "notgood", 1))
            await finds(result.findings[-1])
        else:
            result.add(Finding("vuln", "Not vulnerable to Heartbleed", "", "good", 0))
            await finds(result.findings[-1])
    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:
            result.add(Finding("vuln", "CCS Injection (CVE-2014-0224)", "", "serious", 10))
            await finds(result.findings[-1])
        else:
            result.add(Finding("vuln", "Not vulnerable to CCS Injection", "", "good", 0))
            await finds(result.findings[-1])
    except Exception as e:
        result.data["ccs_injection"] = {"error": str(e)}

    try:
        renego_ok, renego_err = await probe_secure_renegotiation(host, port, sni)
        result.data["secure_renegotiation"] = {"supported": renego_ok, "error": renego_err}
        if renego_ok:
            result.add(Finding("vuln", "Secure renegotiation supported", "", "good", 0))
            await finds(result.findings[-1])
        else:
            result.add(Finding("vuln", "Secure renegotiation not supported", renego_err or "",
                               "bad", 4))
            await finds(result.findings[-1])
    except Exception as e:
        result.data["secure_renegotiation"] = {"error": str(e)}

    try:
        scsv_ok, scsv_err = await probe_fallback_scsv(host, port, sni)
        result.data["fallback_scsv"] = {"supported": scsv_ok, "error": scsv_err}
        if scsv_ok:
            result.add(Finding("vuln", "TLS_FALLBACK_SCSV supported", "", "good", 0))
            await finds(result.findings[-1])
        else:
            result.add(Finding("vuln", "TLS_FALLBACK_SCSV not enforced", scsv_err or "",
                               "notgood", 2))
            await finds(result.findings[-1])
    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)
        result.add(Finding("compat", f"Handshake simulation: {ok_count}/{len(sim)} clients connected",
                           "", "info", 0))
        await finds(result.findings[-1])


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

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

    # TCP reachability first
    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))
        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))
        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))
        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
