from contextlib import contextmanager
import fcntl
import json
import os
from pathlib import Path
import shutil
import tempfile
import time

from backup_core import atomic_json, private_directory
from metric_format import application_samples, disk_usage, number, render
from transport import OperationsError

SERVICES = ["nginx", "app", "queue", "scheduler", "reverb", "mysql", "redis", "meilisearch"]
DISKS = {"mysql": "/var/lib/mysql", "redis": "/data", "meilisearch": "/meili_data"}


class MetricsJob:
    def __init__(self, stack, workspace):
        self.stack = stack
        self.workspace = private_directory(workspace)
        self.state = self.workspace / "collector.json"

    @contextmanager
    def lock(self):
        descriptor = os.open(self.workspace / "metrics.lock", os.O_RDWR | os.O_CREAT, 0o600)
        try:
            try:
                fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
            except BlockingIOError:
                raise OperationsError("Another metrics collection is active") from None
            yield
        finally:
            os.close(descriptor)

    def sample(self, name, value, **labels):
        return {"name": name, "value": number(value), "labels": labels}

    def collect(self):
        previous = json.loads(self.state.read_text()) if self.state.is_file() else {}
        timestamp = int(time.time())
        rows = []
        success = False
        status_success = False
        service_rows = []
        maintenance = (self.workspace / "maintenance.json").is_file()
        try:
            self.stack.model()
            statuses = self.stack.state()
            for service in SERVICES:
                state = statuses.get(service, {})
                service_rows += [self.sample("post76_service_running", int(state.get("State") == "running"), service=service),
                         self.sample("post76_service_healthy", int(state.get("State") == "running" and state.get("Health") == "healthy"), service=service)]
            status_success = True
            rows += service_rows
            raw = self.stack.command(["exec", "-T", "app", "php", "-d", "memory_limit=96M", "docker/metrics.php"], "Application metrics")
            values = json.loads(raw)
            collected = number(values.get("collected_at"))
            if abs(collected - timestamp) > 90:
                raise OperationsError("Application metrics have an unexpected clock")
            rows += application_samples(values)
            for service, path in DISKS.items():
                raw = self.stack.command(["exec", "-T", service, "df", "-Pk", path], "Disk capacity")
                total, free = disk_usage(raw)
                rows += [self.sample("post76_disk_total_bytes", total, volume=service), self.sample("post76_disk_free_bytes", free, volume=service)]
            usage = shutil.disk_usage(self.workspace)
            rows += [self.sample("post76_disk_total_bytes", usage.total, volume="workspace"),
                     self.sample("post76_disk_free_bytes", usage.free, volume="workspace")]
            success = True
        except (OperationsError, ValueError, TypeError, AttributeError, OSError):
            rows = previous.get("samples", [])
            if status_success:
                rows = [row for row in rows if row["name"] not in ["post76_service_running", "post76_service_healthy"]] + service_rows
        last_success = timestamp if success else previous.get("last_success", 0)
        if success:
            atomic_json(self.state, {"last_success": last_success, "samples": rows})
        backup = self.workspace / "status.json"
        backup_values = json.loads(backup.read_text()) if backup.is_file() else {}
        rows = [*rows, self.sample("post76_collector_success", int(success)), self.sample("post76_collector_last_success_timestamp_seconds", last_success),
                self.sample("post76_metrics_publish_timestamp_seconds", timestamp),
                self.sample("post76_service_status_success", int(status_success)),
                self.sample("post76_backup_success", int(backup_values.get("success", False))),
                self.sample("post76_backup_resume_success", int(backup_values.get("resumed", not maintenance))),
                self.sample("post76_backup_last_success_timestamp_seconds", backup_values.get("last_success", 0)),
                self.sample("post76_backup_duration_seconds", backup_values.get("duration", 0)),
                self.sample("post76_backup_maintenance_active", int(maintenance))]
        directory = self.workspace / "metrics"
        directory.mkdir(mode=0o755, exist_ok=True)
        directory.chmod(0o755)
        with tempfile.NamedTemporaryFile(mode="w", dir=directory, prefix=".metrics-", delete=False) as output:
            output.write(render(self.stack.project, rows))
            temporary = output.name
        os.chmod(temporary, 0o644)
        os.replace(temporary, directory / "metrics.prom")
        if not success:
            raise OperationsError("Metrics collection failed; the last good values are retained and marked stale")
