import math

from transport import OperationsError

QUEUES = ["realtime", "push", "mail", "default", "search", "media", "migration"]
SCOPES = ["web", "admin", "api"]
STATES = ["ready", "delayed", "reserved", "expired_reservations"]
CLASSES = ["1xx", "2xx", "3xx", "4xx", "5xx"]


def number(value):
    if type(value) not in [int, float] or not math.isfinite(value) or value < 0 or value > 2**63 - 1:
        raise OperationsError("Invalid aggregate metric value")
    return value


def application_samples(values):
    if values.get("format") != 1 or type(values.get("samples")) is not list:
        raise OperationsError("Invalid application metrics response")
    expected = {("post76_queue_jobs", (("queue", queue), ("state", state))) for queue in QUEUES for state in STATES}
    expected |= {("post76_http_responses_5m", (("scope", scope), ("status_class", status))) for scope in SCOPES for status in CLASSES}
    expected |= {("post76_queue_failed_jobs_5m", ()), ("post76_disk_total_bytes", (("volume", "uploads"),)),
                 ("post76_disk_free_bytes", (("volume", "uploads"),))}
    rows = []
    seen = set()
    for sample in values["samples"]:
        if not isinstance(sample, dict) or set(sample) != {"name", "labels", "value"} or not isinstance(sample["labels"], dict):
            raise OperationsError("Invalid aggregate metric shape")
        try:
            identity = (sample["name"], tuple(sorted(sample["labels"].items())))
            if identity not in expected or identity in seen:
                raise OperationsError("Unexpected or duplicate aggregate metric")
            seen.add(identity)
        except TypeError:
            raise OperationsError("Invalid aggregate metric labels") from None
        rows.append({"name": sample["name"], "labels": sample["labels"], "value": number(sample["value"])})
    if seen != expected:
        raise OperationsError("Application metrics are incomplete")
    capacity = {row["name"]: row["value"] for row in rows if row["name"].startswith("post76_disk_")}
    if capacity["post76_disk_total_bytes"] <= 0 or capacity["post76_disk_free_bytes"] > capacity["post76_disk_total_bytes"]:
        raise OperationsError("Invalid upload capacity metrics")
    return rows


def disk_usage(output):
    try:
        columns = output.decode().strip().splitlines()[-1].split()[-5:]
        total, used, free = [int(value) * 1024 for value in columns[:3]]
        if total <= 0 or free < 0 or used < 0 or free > total:
            raise ValueError()
        return total, free
    except (ValueError, IndexError, UnicodeError):
        raise OperationsError("Invalid disk capacity response") from None


def render(project, rows):
    lines = []
    names = set()
    for row in rows:
        if row["name"] not in names:
            lines += ["# HELP " + row["name"] + " Post76 aggregate operational measurement.", "# TYPE " + row["name"] + " gauge"]
            names.add(row["name"])
        labels = {"project": project, **row["labels"]}
        suffix = ",".join(name + '="' + str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'
                          for name, value in sorted(labels.items()))
        lines.append(row["name"] + "{" + suffix + "} " + str(number(row["value"])))
    return "\n".join(lines) + "\n"
