# retoor <retoor@molodetz.nl>

from __future__ import annotations

import asyncio
import json
from pathlib import Path

from devplacepy.database import get_table
from devplacepy.services.containers import api, store

from . import editor, flags, naming, quota, tunnels

MANIFEST_DIRECTORY = ".devplace"
MANIFEST_NAME = "tunnels.json"
CERT_UNCONFIGURED = (
    "certificate issuance is not configured; an administrator must set the "
    "molohttp base URL and credentials before this address serves HTTPS"
)

PHASE_SUSPENDED = "suspended"
PHASE_STARTING = "starting"
PHASE_READY = "ready"
PHASE_STOPPING = "stopping"
PHASE_STOPPED = "stopped"
PHASE_CRASHED = "crashed"

PHASE_LABELS = {
    PHASE_SUSPENDED: "Suspended",
    PHASE_STARTING: "Starting",
    PHASE_READY: "Ready",
    PHASE_STOPPING: "Stopping",
    PHASE_STOPPED: "Stopped",
    PHASE_CRASHED: "Crashed",
}

TRANSITIONAL_PHASES = (PHASE_STARTING, PHASE_STOPPING)

_pending_certificates: set[asyncio.Task] = set()


class WorkspaceError(Exception):
    pass


def find_for_project(project_uid: str, owner_uid: str) -> dict | None:
    return get_table("instances").find_one(
        project_uid=project_uid,
        workspace_owner_uid=owner_uid,
        is_workspace=1,
        deleted_at=None,
    )


def count_for_owner(owner_uid: str) -> int:
    return get_table("instances").count(
        workspace_owner_uid=owner_uid, is_workspace=1, deleted_at=None
    )


async def ensure(project: dict, user: dict) -> dict:
    owner_uid = user["uid"]
    existing = find_for_project(project["uid"], owner_uid)
    if existing:
        return existing
    limits = quota.resolve(owner_uid)
    if (
        not limits.unlimited
        and limits.max_workspaces
        and count_for_owner(owner_uid) >= limits.max_workspaces
    ):
        raise WorkspaceError(
            f"workspace limit reached ({limits.max_workspaces}); "
            "delete one before creating another"
        )
    instance = await api.create_instance(
        project,
        name=f"ws-{project.get('slug') or project['uid']}"[:64],
        actor=("user", owner_uid),
        ports=[f"{api.EDITOR_DEFAULT_PORT}"],
    )
    store.update_instance(
        instance["uid"],
        {
            "is_workspace": 1,
            "workspace_owner_uid": owner_uid,
            "editor_port": api.EDITOR_DEFAULT_PORT,
            "tunnel_name": naming.generate(instance["uid"]),
            "desired_state": "running",
        },
    )
    instance = store.get_instance(instance["uid"])
    api.ensure_editor_password(instance)
    schedule_certificate(publish_editor_tunnel(instance, owner_uid))
    return store.get_instance(instance["uid"])


def publish_tunnel(
    instance: dict, label: str, container_port: int, owner_uid: str
) -> dict:
    if container_port <= 0 or container_port > 65535:
        raise WorkspaceError("container_port must be between 1 and 65535")
    limits = quota.resolve(instance.get("workspace_owner_uid", ""), instance)
    if (
        not limits.unlimited
        and limits.max_tunnels
        and tunnels.count_for_instance(instance["uid"]) >= limits.max_tunnels
    ):
        raise WorkspaceError(f"tunnel limit reached ({limits.max_tunnels})")
    row = tunnels.create(instance, label, container_port, owner_uid)
    if not row:
        raise WorkspaceError("could not create tunnel")
    schedule_certificate(row)
    write_manifest(instance)
    return tunnels.get(row["uid"]) or row


def schedule_certificate(tunnel: dict | None) -> bool:
    from . import certs

    if not tunnel or tunnels.keeps_certificate(tunnel):
        return False
    if not certs.configured():
        tunnels.update(tunnel["uid"], {"last_error": CERT_UNCONFIGURED})
        return False
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return False
    tunnels.update(
        tunnel["uid"], {"status": tunnels.STATUS_PROVISIONING, "last_error": ""}
    )
    task = loop.create_task(certs.certify(tunnel))
    _pending_certificates.add(task)
    task.add_done_callback(_pending_certificates.discard)
    return True


def publish_editor_tunnel(instance: dict, owner_uid: str) -> dict | None:
    port = int(instance.get("editor_port") or api.EDITOR_DEFAULT_PORT)
    if not port or not instance.get("tunnel_name"):
        return None
    return tunnels.create(instance, "Editor", port, owner_uid)


def is_editor_tunnel(instance: dict, tunnel: dict) -> bool:
    editor_port = int(instance.get("editor_port") or api.EDITOR_DEFAULT_PORT)
    return int(tunnel.get("container_port") or 0) == editor_port


def resume(instance: dict) -> dict:
    if instance.get("suspended_at"):
        raise WorkspaceError("this workspace is suspended; contact an administrator")
    limits = quota.resolve(instance.get("workspace_owner_uid", ""), instance)
    disk_quota = limits.disk_quota_bytes()
    if disk_quota and int(instance.get("disk_bytes") or 0) >= disk_quota:
        raise WorkspaceError(
            "disk quota reached; free space before starting this workspace again"
        )
    store.update_instance(
        instance["uid"],
        {
            "desired_state": "running",
            "last_active_at": store.now(),
            "idle_warned_at": "",
            "delete_warned_at": "",
        },
    )
    return store.get_instance(instance["uid"])


def stop(instance: dict) -> dict:
    store.update_instance(instance["uid"], {"desired_state": "stopped"})
    return store.get_instance(instance["uid"])


def suspend(instance: dict, actor_uid: str, reason: str) -> dict:
    from datetime import datetime, timezone

    store.update_instance(
        instance["uid"],
        {
            "desired_state": "stopped",
            "suspended_at": datetime.now(timezone.utc).isoformat(),
            "suspended_by": actor_uid,
            "flag_reason": reason,
        },
    )
    tunnels.suspend_for_instance(instance["uid"])
    return store.get_instance(instance["uid"])


def unsuspend(instance: dict) -> dict:
    store.update_instance(
        instance["uid"], {"suspended_at": "", "suspended_by": "", "flag_reason": ""}
    )
    tunnels.resume_for_instance(instance["uid"])
    return store.get_instance(instance["uid"])


def editor_target(instance: dict) -> tuple[str, int]:
    port = int(instance.get("editor_port") or api.EDITOR_DEFAULT_PORT)
    return api.tunnel_target(instance, port)


def manifest_payload(instance: dict) -> dict:
    rows = tunnels.list_for_instance(instance["uid"])
    return {
        "workspace": {
            "uid": instance.get("uid", ""),
            "name": instance.get("name", ""),
            "tunnel_name": instance.get("tunnel_name", ""),
            "domain": naming.domain(),
            "project_uid": instance.get("project_uid", ""),
        },
        "tunnels": [
            {
                "label": row.get("label", ""),
                "hostname": row.get("hostname", ""),
                "url": f"https://{row.get('hostname', '')}",
                "container_port": int(row.get("container_port") or 0),
                "status": row.get("status", ""),
                "cert_status": row.get("cert_status", ""),
            }
            for row in rows
        ],
    }


def write_manifest(instance: dict) -> None:
    workspace_dir = instance.get("workspace_dir")
    if not workspace_dir:
        return
    directory = Path(workspace_dir) / MANIFEST_DIRECTORY
    try:
        directory.mkdir(parents=True, exist_ok=True)
        (directory / MANIFEST_NAME).write_text(
            json.dumps(manifest_payload(instance), indent=2)
        )
    except OSError:
        return


async def editor_ready(instance: dict) -> bool:
    if instance.get("suspended_at"):
        return False
    if instance.get("status") != store.ST_RUNNING:
        return False
    return await api.editor_reachable(instance)


def phase(instance: dict, ready: bool) -> str:
    if instance.get("suspended_at"):
        return PHASE_SUSPENDED
    status = instance.get("status") or ""
    if instance.get("desired_state") == store.DESIRED_RUNNING:
        if status == store.ST_RUNNING and ready:
            return PHASE_READY
        return PHASE_STARTING
    if status in (store.ST_RUNNING, store.ST_PAUSED, store.ST_RESTARTING):
        return PHASE_STOPPING
    if status == store.ST_CRASHED:
        return PHASE_CRASHED
    return PHASE_STOPPED


async def view(instance: dict) -> dict:
    owner_uid = instance.get("workspace_owner_uid", "")
    limits = quota.resolve(owner_uid, instance)
    disk_used = int(instance.get("disk_bytes") or 0)
    egress_used = int(instance.get("egress_bytes") or 0)
    ready = await editor_ready(instance)
    current = phase(instance, ready)
    return {
        "uid": instance.get("uid", ""),
        "name": instance.get("name", ""),
        "owner_uid": owner_uid,
        "status": instance.get("status", ""),
        "desired_state": instance.get("desired_state", ""),
        "phase": current,
        "phase_label": PHASE_LABELS[current],
        "editor_ready": ready,
        "suspended": bool(instance.get("suspended_at")),
        "flag_reason": instance.get("flag_reason", ""),
        "tunnel_name": instance.get("tunnel_name", ""),
        "primary_url": (
            f"https://{naming.hostname_for(instance.get('tunnel_name', ''))}"
            if instance.get("tunnel_name")
            else ""
        ),
        "last_active_at": instance.get("last_active_at", ""),
        "disk_bytes": disk_used,
        "disk_quota_mb": limits.disk_quota_mb,
        "disk_percent": quota.percent_used(disk_used, limits.disk_quota_bytes()),
        "egress_bytes": egress_used,
        "egress_quota_mb": limits.egress_quota_mb,
        "egress_percent": quota.percent_used(egress_used, limits.egress_quota_bytes()),
        "idle_stop_minutes": limits.idle_stop_minutes,
        "retention_days": limits.retention_days,
        "max_tunnels": limits.max_tunnels,
        "unlimited": limits.unlimited,
        "tunnels": tunnels.list_for_instance(instance["uid"]),
        "flags": flags.list_flags(instance_uid=instance["uid"]),
        "editor": editor.view(owner_uid, instance),
    }
