"""`manifest.csv`（バンドルの 1 行 1 ファイルの表）の組み立て（SPEC_03「manifest.csv の列」）.

版の作成（`datasets`）とバンドルの書き出し・読み込み（`bundle`）が同じ形を使うため，
目録（`catalog`）・ストア・設定に依存しない純関数だけを置く（計算サーバで import しても
目録を引き込まないようにするため．SPEC_01「依存の向き」）．
"""

import csv
import hashlib
import io
import json
from collections.abc import Iterable, Mapping, Sequence
from typing import Any

from pydantic import BaseModel, ConfigDict

# 先頭の固定の列（この後に `<所見>.<項目>` の列が定義の `[columns]` の順で並ぶ）
BASE_COLUMNS: tuple[str, ...] = ("path", "sha256", "kind", "subject_id", "capture_id")
FILES_DIRNAME = "files"
MANIFEST_ENCODING = "utf-8"


class ManifestEntry(BaseModel):
    """manifest の 1 行の元になる情報（版に含まれる 1 ファイル）．

    Attributes:
        sha256: ファイルの SHA-256．
        ext: 拡張子（小文字．`.` なし）．
        kind: ファイルの種類．
        subject_id: 研究用の症例 ID．
        capture_id: 撮影 ID．
        annotations: 採用した所見（所見の種類 → 値の辞書）．無い種類は含めない．
    """

    model_config = ConfigDict(frozen=True)

    sha256: str
    ext: str
    kind: str
    subject_id: str
    capture_id: str
    annotations: dict[str, dict[str, Any]] = {}


def bundle_file_path(sha256: str, ext: str) -> str:
    """バンドルのルートからのファイルの相対パス（`files/<sha256>.<ext>`．区切りは `/`）．"""
    return f"{FILES_DIRNAME}/{sha256}.{ext}"


def manifest_columns(columns: Mapping[str, Sequence[str]]) -> list[str]:
    """manifest の列名を返す．

    Args:
        columns: 所見の種類 → 出す項目（データセットの定義の `[columns]`．この順に並べる）．

    Returns:
        固定の列のあとに `<所見>.<項目>` を並べた列名．
    """
    return [
        *BASE_COLUMNS,
        *(f"{kind}.{field}" for kind, fields in columns.items() for field in fields),
    ]


def _cell(value: object) -> str:
    # 文字列はそのまま，無い値は空，それ以外（数値・真偽値等）は JSON の表記にする
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def _sort_key(entry: ManifestEntry) -> tuple[str, str, str]:
    return (entry.subject_id, entry.capture_id, entry.sha256)


def build_manifest(entries: Iterable[ManifestEntry], columns: Mapping[str, Sequence[str]]) -> bytes:
    """manifest.csv のバイト列を作る．

    行は `subject_id`・`capture_id`・`sha256` の順に並べ，UTF-8（BOM なし）・改行 LF で書く
    （同じ構成なら同じバイト列になるようにするため．SPEC_03「版の作成」）．
    患者 ID・元のファイル名は入力に含まれないので出ない．

    Args:
        entries: 版に含まれるファイル．
        columns: 所見の種類 → 出す項目（定義の `[columns]`）．

    Returns:
        manifest.csv の中身．
    """
    buffer = io.StringIO(newline="")
    writer = csv.writer(buffer, lineterminator="\n")
    writer.writerow(manifest_columns(columns))
    for entry in sorted(entries, key=_sort_key):
        values = [
            _cell(entry.annotations.get(kind, {}).get(field))
            for kind, fields in columns.items()
            for field in fields
        ]
        writer.writerow(
            [
                bundle_file_path(entry.sha256, entry.ext),
                entry.sha256,
                entry.kind,
                entry.subject_id,
                entry.capture_id,
                *values,
            ]
        )
    return buffer.getvalue().encode(MANIFEST_ENCODING)


def manifest_sha256(data: bytes) -> str:
    """manifest.csv のバイト列の SHA-256（版の指紋．16 進 64 文字の小文字）．"""
    return hashlib.sha256(data).hexdigest()
