#!/usr/bin/env python3
"""B-07境界台帳を検証し、順位付けを含まない要約を生成する。"""

from __future__ import annotations

import argparse
import csv
from collections import Counter, defaultdict
from datetime import date
from pathlib import Path
from urllib.parse import urlparse


FIELDS = {
    "sender_input_history",
    "recipient_onchain_link",
    "transaction_amount",
    "wallet_source_ip",
    "transaction_presence_and_time",
    "wallet_keys_and_local_plaintext",
    "message_content",
    "contact_graph",
    "message_or_file_size",
    "device_ip",
    "activity_timing",
    "decrypted_history_on_endpoint",
    "file_content_and_names",
    "vault_format_presence",
    "stored_file_size",
    "cloud_sync_network_origin",
    "file_timestamps_and_counts",
    "plaintext_on_unlocked_endpoint",
}
TOOLS = {"Monero", "SimpleX", "Cryptomator"}
ASSESSMENTS = {"protected", "partial", "exposed", "unknown"}
ALLOWED_SOURCE_HOSTS = {
    "docs.getmonero.org",
    "www.getmonero.org",
    "simplex.chat",
    "docs.cryptomator.org",
}
REQUIRED = {
    "id",
    "tool",
    "layer",
    "field",
    "default_assessment",
    "hardened_assessment",
    "observer",
    "evidence_basis",
    "source_url",
    "as_of",
    "limitation",
}


def validate(path: Path) -> list[dict[str, str]]:
    with path.open("r", encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle)
        if set(reader.fieldnames or []) != REQUIRED:
            raise ValueError(f"unexpected columns: {reader.fieldnames}")
        rows = list(reader)

    if len(rows) != 18:
        raise ValueError(f"expected 18 rows, found {len(rows)}")

    ids: set[str] = set()
    seen_fields: set[str] = set()
    tool_counts: Counter[str] = Counter()
    for row in rows:
        if not all(row[column].strip() for column in REQUIRED):
            raise ValueError(f"blank required value in {row.get('id', '<unknown>')}")
        if row["id"] in ids:
            raise ValueError(f"duplicate id: {row['id']}")
        ids.add(row["id"])
        if row["tool"] not in TOOLS:
            raise ValueError(f"unknown tool: {row['tool']}")
        tool_counts[row["tool"]] += 1
        if row["field"] not in FIELDS or row["field"] in seen_fields:
            raise ValueError(f"unexpected or duplicate field: {row['field']}")
        seen_fields.add(row["field"])
        for column in ("default_assessment", "hardened_assessment"):
            if row[column] not in ASSESSMENTS:
                raise ValueError(f"invalid {column}: {row[column]}")
        parsed = urlparse(row["source_url"])
        if parsed.scheme != "https" or parsed.hostname not in ALLOWED_SOURCE_HOSTS:
            raise ValueError(f"unapproved source: {row['source_url']}")
        date.fromisoformat(row["as_of"])

    if tool_counts != Counter({tool: 6 for tool in TOOLS}):
        raise ValueError(f"expected six rows per tool, found {dict(tool_counts)}")
    if seen_fields != FIELDS:
        raise ValueError("field coverage mismatch")
    return rows


def summary(rows: list[dict[str, str]]) -> str:
    by_tool: dict[str, Counter[str]] = defaultdict(Counter)
    for row in rows:
        by_tool[row["tool"]][row["default_assessment"]] += 1

    lines = [
        "B-07 privacy-layer boundary audit",
        "Rows: 18 (3 tools x 6 observable fields)",
        "Method: source-bound boundary classification; counts are not a privacy score or ranking.",
        "As-of: 2026-08-13",
        "",
    ]
    for tool in ("Monero", "SimpleX", "Cryptomator"):
        counts = by_tool[tool]
        rendered = ", ".join(
            f"{state}={counts[state]}" for state in sorted(ASSESSMENTS)
        )
        lines.append(f"{tool}: {rendered}")
    lines.extend(
        [
            "",
            "Interpretation boundary: a protected field is protected only from the named observer and only within the documented scope.",
            "Unknown means the cited source does not establish the property; it is not evidence of exposure or protection.",
        ]
    )
    return "\n".join(lines) + "\n"


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("csv_path", type=Path)
    parser.add_argument("--out", type=Path)
    args = parser.parse_args()
    rows = validate(args.csv_path)
    rendered = summary(rows)
    if args.out:
        args.out.write_text(rendered, encoding="utf-8", newline="\n")
    print(rendered, end="")
    return 0


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