#!/usr/bin/env python3
"""Summarize Docker's machine-readable stats without applying pass/fail limits."""

from __future__ import annotations

import argparse
import json
import re
from collections import defaultdict
from decimal import Decimal
from pathlib import Path
from statistics import fmean


UNIT_BYTES = {
    "B": Decimal(1),
    "kB": Decimal(1_000),
    "MB": Decimal(1_000_000),
    "GB": Decimal(1_000_000_000),
    "TB": Decimal(1_000_000_000_000),
    "KiB": Decimal(1_024),
    "MiB": Decimal(1_048_576),
    "GiB": Decimal(1_073_741_824),
    "TiB": Decimal(1_099_511_627_776),
}


def parse_size(value: str) -> int:
    match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]+)", value.strip())
    if match is None:
        raise ValueError(f"invalid Docker size value: {value}")
    number, unit = match.groups()
    try:
        multiplier = UNIT_BYTES[unit]
    except KeyError as error:
        raise ValueError(f"unsupported Docker size unit: {unit}") from error
    return int(Decimal(number) * multiplier)


def parse_memory_usage(value: str) -> int:
    used, separator, _limit = value.partition("/")
    if not separator:
        raise ValueError(f"invalid Docker MemUsage value: {value}")
    return parse_size(used)


def parse_cpu(value: str) -> float:
    if not value.endswith("%"):
        raise ValueError(f"invalid Docker CPUPerc value: {value}")
    return float(value.removesuffix("%"))


def summarize(source: Path) -> dict[str, object]:
    samples: dict[str, list[dict[str, object]]] = defaultdict(list)
    observed_at: list[str] = []

    with source.open(encoding="utf-8") as stream:
        for line_number, line in enumerate(stream, start=1):
            if not line.strip():
                continue

            try:
                row = json.loads(line)
                name = str(row["Name"])
                timestamp = str(row["ObservedAt"])
                samples[name].append(
                    {
                        "observed_at": timestamp,
                        "cpu_percent": parse_cpu(str(row["CPUPerc"])),
                        "memory_bytes": parse_memory_usage(str(row["MemUsage"])),
                        "pids": int(row["PIDs"]),
                    }
                )
                observed_at.append(timestamp)
            except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
                raise ValueError(f"{source}:{line_number}: {error}") from error

    if not samples:
        raise ValueError(f"{source}: no Docker stats samples")

    containers: list[dict[str, object]] = []

    for name in sorted(samples):
        rows = samples[name]
        cpu_values = [float(row["cpu_percent"]) for row in rows]
        memory_values = [int(row["memory_bytes"]) for row in rows]
        pid_values = [int(row["pids"]) for row in rows]
        window_size = min(10, len(rows))
        window_count = min(10, len(rows))
        memory_windows: list[dict[str, object]] = []

        for window_index in range(window_count):
            start = window_index * len(rows) // window_count
            end = (window_index + 1) * len(rows) // window_count
            window_rows = rows[start:end]
            window_memory = [int(row["memory_bytes"]) for row in window_rows]
            memory_windows.append(
                {
                    "index": window_index + 1,
                    "sample_count": len(window_rows),
                    "first_observed_at": str(window_rows[0]["observed_at"]),
                    "last_observed_at": str(window_rows[-1]["observed_at"]),
                    "average": fmean(window_memory),
                    "minimum": min(window_memory),
                    "maximum": max(window_memory),
                    "first": window_memory[0],
                    "last": window_memory[-1],
                }
            )

        containers.append(
            {
                "name": name,
                "sample_count": len(rows),
                "cpu_percent": {
                    "average": fmean(cpu_values),
                    "minimum": min(cpu_values),
                    "maximum": max(cpu_values),
                },
                "memory_bytes": {
                    "average": fmean(memory_values),
                    "minimum": min(memory_values),
                    "maximum": max(memory_values),
                    "first": memory_values[0],
                    "last": memory_values[-1],
                    "first_ten_average": fmean(memory_values[:window_size]),
                    "last_ten_average": fmean(memory_values[-window_size:]),
                    "first_to_last_delta": memory_values[-1] - memory_values[0],
                    "sequential_windows": memory_windows,
                },
                "pids": {
                    "minimum": min(pid_values),
                    "maximum": max(pid_values),
                    "first": pid_values[0],
                    "last": pid_values[-1],
                },
            }
        )

    return {
        "schema_version": 2,
        "measurement": (
            "docker stats --no-stream; the sampler sleeps one second after "
            "each complete multi-container collection"
        ),
        "thresholds_applied": False,
        "first_observed_at": min(observed_at),
        "last_observed_at": max(observed_at),
        "containers": containers,
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("source", type=Path)
    parser.add_argument("destination", type=Path)
    args = parser.parse_args()

    summary = summarize(args.source)
    args.destination.write_text(
        json.dumps(summary, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


if __name__ == "__main__":
    main()
