"""GPU の観測結果（サンプリング・pmon）の解析と，使い方の型の判定（純関数）.

観測は probe コマンドがサーバ側で行い（`build_probe_command`），ここはその出力文字列を
解釈するだけにする．使い方の型は表示専用で，空き判定には使わない（SPEC_03「使い方の型」）．
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from enum import StrEnum

# 0% を超えたサンプルがこの割合以上なら「常時使用」
CONSTANT_USE_RATIO = 0.8
# nvidia-smi --query-gpu=index,utilization.gpu,memory.used の列数
_SAMPLE_COLUMNS = 3


class UsagePattern(StrEnum):
    """観測中の使用率から付ける使い方の型（表示専用）."""

    CONSTANT = "常時使用"
    INTERMITTENT = "断続的に使用"
    MEMORY_ONLY = "メモリ確保のみ"
    IDLE = "未使用"


@dataclass(frozen=True)
class GpuSample:
    """GPU 1 基の 1 回分のサンプル.

    Attributes:
        utilization_percent: 使用率（%）．
        memory_used_mib: 使用中メモリ（MiB）．
    """

    utilization_percent: int
    memory_used_mib: int


@dataclass(frozen=True)
class GpuUsage:
    """観測中のサンプルの要約.

    Attributes:
        sample_count: サンプル数（1 以上）．
        active_count: 使用率が 0% を超えたサンプル数．
        mean_utilization_percent: 使用率の平均（%）．
        max_utilization_percent: 使用率の最大（%）．
        max_memory_used_mib: 使用中メモリの最大（MiB）．
    """

    sample_count: int
    active_count: int
    mean_utilization_percent: float
    max_utilization_percent: int
    max_memory_used_mib: int


def _parse_int(text: str) -> int | None:
    """整数として解釈できれば返す（`[N/A]`・`-` 等は None）."""
    try:
        return int(text.strip())
    except ValueError:
        return None


def parse_gpu_samples(text: str) -> dict[int, list[GpuSample]]:
    """観測のサンプリング出力（`index, 使用率, 使用中メモリ` の CSV）を GPU ごとにまとめる.

    `timeout` で打ち切った最後の行が途中で切れていることがあるため，解釈できない行は
    読み飛ばす（そのぶんサンプルが減るだけで，サンプルが 0 件の GPU は状態不明になる）．

    Args:
        text: サンプリングの出力．

    Returns:
        GPU 番号からサンプル（出力順）への対応．
    """
    samples: dict[int, list[GpuSample]] = {}
    for line in text.splitlines():
        columns = line.split(",")
        if len(columns) != _SAMPLE_COLUMNS:
            continue
        index, util, memory = (_parse_int(column) for column in columns)
        if index is None or util is None or memory is None:
            continue
        samples.setdefault(index, []).append(GpuSample(util, memory))
    return samples


def summarize_samples(samples: Sequence[GpuSample]) -> GpuUsage | None:
    """サンプルを要約する.

    Args:
        samples: GPU 1 基のサンプル．

    Returns:
        要約．サンプルが無ければ None．
    """
    if not samples:
        return None
    utils = [sample.utilization_percent for sample in samples]
    return GpuUsage(
        sample_count=len(samples),
        active_count=sum(1 for util in utils if util > 0),
        mean_utilization_percent=sum(utils) / len(utils),
        max_utilization_percent=max(utils),
        max_memory_used_mib=max(sample.memory_used_mib for sample in samples),
    )


def classify_usage_pattern(usage: GpuUsage, process_count: int | None) -> UsagePattern | None:
    """観測中の使用率から使い方の型を付ける（表示専用）.

    Args:
        usage: 観測の要約．
        process_count: 計算プロセスの数．一覧が取れなければ None．

    Returns:
        使い方の型．0% を超えたサンプルが無く，計算プロセスの有無も分からなければ None．
    """
    if usage.active_count > 0:
        if usage.active_count >= CONSTANT_USE_RATIO * usage.sample_count:
            return UsagePattern.CONSTANT
        return UsagePattern.INTERMITTENT
    if process_count is None:
        return None
    return UsagePattern.MEMORY_ONLY if process_count > 0 else UsagePattern.IDLE


def parse_pmon(text: str) -> dict[int, int] | None:
    """`nvidia-smi pmon -s u` の出力から，計算プロセスごとの SM 使用率の最大を求める.

    `#` で始まる行はヘッダ（1 行目が列名 `# gpu pid type sm ...`，2 行目が単位）．列の位置は
    ヘッダの列名から決める．種別に `C`（計算）を含む行だけを数え，`G`（描画）だけの行は除く．
    値が `-` の行（その回はプロセスが無い・値が取れない）は読み飛ばす．

    Args:
        text: pmon の出力．

    Returns:
        pid から SM 使用率の最大（%）への対応．列名のヘッダが無い（pmon が失敗・未対応）
        ときは None（表示を省くだけで，判定には影響させない）．
    """
    columns: list[str] | None = None
    peaks: dict[int, int] = {}
    for line in text.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        if stripped.startswith("#"):
            names = stripped.lstrip("#").split()
            if columns is None and {"pid", "type", "sm"} <= set(names):
                columns = names
            continue
        if columns is None:
            continue
        values = stripped.split()
        if len(values) < len(columns) - 1:
            continue
        row = dict(zip(columns, values, strict=False))
        pid = _parse_int(row.get("pid", "-"))
        sm = _parse_int(row.get("sm", "-"))
        if pid is None or sm is None or "C" not in row.get("type", ""):
            continue
        peaks[pid] = max(peaks.get(pid, 0), sm)
    return None if columns is None else peaks


def format_elapsed(seconds: int) -> str:
    """経過秒数を読みやすい単位で返す（例: `45 秒`・`12.5 分`・`27.7 時間`・`3.2 日`）.

    48 時間までは時間で示す（日単位にすると 1 日前後の違いが読み取りにくいため）．

    Args:
        seconds: 経過秒数（0 以上）．

    Returns:
        表示用の文字列．
    """
    if seconds < 60:
        return f"{seconds} 秒"
    if seconds < 3600:
        return f"{seconds / 60:.1f} 分"
    if seconds < 48 * 3600:
        return f"{seconds / 3600:.1f} 時間"
    return f"{seconds / 86400:.1f} 日"
