"""記事掲載用の小さなCSV入出力ヘルパー。ネットワーク通信はしません。"""
from pathlib import Path
from datetime import datetime, timezone
import hashlib
import json
import uuid
import importlib.metadata
import pandas as pd

def digest(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()

def versions(names):
    result = {}
    for name in names:
        try:
            result[name] = importlib.metadata.version(name)
        except importlib.metadata.PackageNotFoundError:
            result[name] = "not installed"
    return result

def load_reviews(path):
    df = pd.read_csv(path, dtype=str, keep_default_na=False, encoding="utf-8-sig")
    required = {"content", "score", "at"}
    if not required <= set(df):
        raise ValueError(f"必要列がありません: {sorted(required - set(df))}。レビュー取得記事のCSVを確認してください。")
    if "row_id" in df:
        df = df.rename(columns={"row_id": "input_row_id"})
    df.insert(0, "row_id", range(len(df)))
    for col in ["reviewId", "reviewCreatedVersion", "appVersion"]:
        if col not in df:
            df[col] = ""
    df["content_raw"] = df["content"]
    df["at_raw"] = df["at"]
    df["score_raw"] = df["score"]
    df["score_valid"] = pd.to_numeric(df["score"], errors="coerce")
    df.loc[~df["score_valid"].isin([1, 2, 3, 4, 5]), "score_valid"] = float("nan")
    # オフセット付き日時だけUTCへ変換。旧CSVのnaive日時は壁時計のまま別集団にする。
    def date_parts(value):
        try:
            t = pd.Timestamp(value)
            if pd.isna(t):
                raise ValueError("missing")
            basis = "UTC" if t.tzinfo is not None else "timezone_unknown_wall_clock"
            if t.tzinfo is not None:
                t = t.tz_convert("UTC")
            return (t.strftime("%Y-%m-%d"), t.strftime("%Y-%m"), basis)
        except (ValueError, TypeError, OverflowError):
            return ("", "", "invalid_or_missing")
    parts = [date_parts(v) for v in df["at"]]
    for i, col in enumerate(["day", "month", "date_basis"]):
        df[col] = [p[i] for p in parts]
    # 1.2.7では両項目の抽出元は同じ。しかし旧CSVの由来を保証しないため自動補完しない。
    df["version"] = df["reviewCreatedVersion"].str.strip().replace("", "unknown")
    df["version_source"] = df["version"].map(lambda v: "unknown" if v == "unknown" else "reviewCreatedVersion")
    return df

class Run:
    def __init__(self, kind, input_path=None, settings=None, root="outputs/gp_runs"):
        stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
        self.path = Path(root) / f"{kind}-{stamp}-{uuid.uuid4().hex[:8]}"
        self.path.mkdir(parents=True, exist_ok=False)
        self.meta = {"run_id": self.path.name, "kind": kind,
                     "started_at_utc": datetime.now(timezone.utc).isoformat(),
                     "input_file": str(input_path) if input_path else None,
                     "input_sha256": digest(input_path) if input_path else None,
                     "settings": settings or {}, "files": {}, "skipped": {},
                     "versions": versions(["pandas", "plotly", "Janome", "scikit-learn", "bertopic", "google-play-scraper"])}
        if input_path:
            source = Path(input_path)
            self.meta["source_metadata"] = {"status": "not_available"}
            for receipt in [source.with_suffix(".metadata.json"), source.parent / "metadata.json"]:
                if receipt.is_file():
                    try:
                        provenance = json.loads(receipt.read_text(encoding="utf-8"))
                        matches = provenance.get("files", {}).get(source.name) == digest(source)
                        self.meta["source_metadata"] = {"path": str(receipt), "sha256": digest(receipt),
                            "status": "matching_csv_hash" if matches else "not_matching_or_no_csv_hash",
                            "settings": provenance.get("settings") if matches else None,
                            "started_at_utc": provenance.get("started_at_utc") if matches else None,
                            "finished_at_utc": provenance.get("finished_at_utc") if matches else None}
                        if matches:
                            break
                    except (ValueError, OSError):
                        self.meta["source_metadata"] = {"status": "unreadable"}

    def csv(self, name, frame):
        frame.to_csv(self.path / name, index=False, encoding="utf-8-sig")
        self.record(name)

    def record(self, name):
        self.meta["files"][name] = digest(self.path / name)

    def figure(self, name, fig):
        fig.update_layout(autosize=True, margin=dict(l=45, r=20, t=85, b=130))
        fig.write_html(self.path / name, include_plotlyjs=True, full_html=True,
                       config={"responsive": True})
        self.record(name)

    def finish(self, **extra):
        self.meta.update(extra)
        self.meta["finished_at_utc"] = datetime.now(timezone.utc).isoformat()
        (self.path / "metadata.json").write_text(json.dumps(self.meta, ensure_ascii=False, indent=2), encoding="utf-8")
        print(self.path)
        return self.path

def checked_run(folder):
    folder = Path(folder)
    meta = json.loads((folder / "metadata.json").read_text(encoding="utf-8"))
    if meta["run_id"] != folder.name:
        raise ValueError("実行フォルダー名とmetadataが一致しません。")
    for name, expected in meta["files"].items():
        if Path(name).name != name or digest(folder / name) != expected:
            raise ValueError(f"出力の取り違え・変更: {name}")
    return folder, meta
