# retoor <retoor@molodetz.nl>

from __future__ import annotations

import json
import logging
import shlex
from dataclasses import asdict, dataclass
from pathlib import Path

from devplacepy import config
from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.services.containers import store
from devplacepy.services.containers.backend.base import (
    WORKSPACE_MOUNT,
    WORKSPACE_STATE_MOUNT,
)

from . import quota

logger = logging.getLogger(__name__)

PREFS_TABLE = "workspace_editor_prefs"

APP_NAME = "DevPlace"
WELCOME_TEXT = "Sign in to your DevPlace workspace"
EDITOR_DEFAULT_PORT = 8443
PROFILE_FILE = "devplace-editor.json"
MANAGED_FILE = ".devplace-managed.json"

INHERIT_TEXT = ""
INHERIT_INT = 0
INHERIT_ZOOM = -99
INHERIT_FLAG = -1

THEME_CHOICES = ("devplace-dark", "devplace-light", "system")
LAYOUT_CHOICES = ("standard", "terminal-focus", "zen")
PANEL_CHOICES = ("short", "normal", "tall", "maximized")
WINDOW_CHOICES = ("tab", "window", "fullscreen")
AGENT_CHOICES = ("dpc", "none")

CHOICES = {
    "theme": THEME_CHOICES,
    "layout": LAYOUT_CHOICES,
    "panel_preset": PANEL_CHOICES,
    "window_mode": WINDOW_CHOICES,
    "boot_agent": AGENT_CHOICES,
}

SENTINELS = {
    "zoom_level": INHERIT_ZOOM,
    "boot_shell": INHERIT_FLAG,
}

THEME_LABELS = {
    "devplace-dark": "DevPlace Dark",
    "devplace-light": "DevPlace Light",
}

LAYOUTS = {
    "standard": {
        "workbench.activityBar.location": "default",
        "workbench.sideBar.location": "left",
        "workbench.panel.defaultLocation": "bottom",
        "editor.minimap.enabled": True,
        "breadcrumbs.enabled": True,
    },
    "terminal-focus": {
        "workbench.activityBar.location": "top",
        "workbench.sideBar.location": "left",
        "workbench.panel.defaultLocation": "bottom",
        "editor.minimap.enabled": False,
        "breadcrumbs.enabled": True,
    },
    "zen": {
        "workbench.activityBar.location": "hidden",
        "workbench.sideBar.location": "left",
        "workbench.panel.defaultLocation": "bottom",
        "editor.minimap.enabled": False,
        "breadcrumbs.enabled": False,
    },
}

FOREIGN_AI_SETTINGS = {
    "chat.disableAIFeatures": True,
    "chat.commandCenter.enabled": False,
    "workbench.secondarySideBar.defaultVisibility": "hidden",
}

TRUST_SETTINGS = {
    "security.workspace.trust.enabled": False,
    "security.workspace.trust.startupPrompt": "never",
    "security.workspace.trust.banner": "never",
    "security.workspace.trust.emptyWindow": True,
    "security.workspace.trust.untrustedFiles": "open",
    "task.allowAutomaticTasks": "on",
}

BOUNDS = {
    "font_size": (8, 48),
    "terminal_font_size": (8, 48),
    "zoom_level": (-5, 5),
    "window_width": (640, 7680),
    "window_height": (480, 4320),
}

SETTING_KEYS = {
    "trust_all": "workspace_editor_trust_all",
    "theme": "workspace_editor_theme",
    "font_size": "workspace_editor_font_size",
    "terminal_font_size": "workspace_editor_terminal_font_size",
    "zoom_level": "workspace_editor_zoom_level",
    "layout": "workspace_editor_layout",
    "panel_preset": "workspace_editor_panel_preset",
    "boot_agent": "workspace_editor_boot_agent",
    "boot_shell": "workspace_editor_boot_shell",
    "window_mode": "workspace_editor_window_mode",
    "window_width": "workspace_editor_window_width",
    "window_height": "workspace_editor_window_height",
}

DEFAULTS = {
    "trust_all": True,
    "theme": "devplace-dark",
    "font_size": 14,
    "terminal_font_size": 13,
    "zoom_level": 0,
    "layout": "standard",
    "panel_preset": "normal",
    "boot_agent": "dpc",
    "boot_shell": True,
    "window_mode": "tab",
    "window_width": 1600,
    "window_height": 1000,
}

PREF_COLUMNS = (
    "font_size",
    "terminal_font_size",
    "zoom_level",
    "theme",
    "layout",
    "panel_preset",
    "window_mode",
    "window_width",
    "window_height",
    "boot_agent",
    "boot_shell",
)

OPTIONAL_FLAGS = (
    "--app-name",
    "--welcome-text",
    "--disable-getting-started-override",
    "--disable-workspace-trust",
)

SOURCE_SITE = "site"
SOURCE_USER = "user"


@dataclass(frozen=True)
class EditorProfile:
    trust_all: bool
    theme: str
    font_size: int
    terminal_font_size: int
    zoom_level: int
    layout: str
    panel_preset: str
    boot_agent: str
    boot_shell: bool
    window_mode: str
    window_width: int
    window_height: int
    cpu_millicores: int
    memory_mb: int
    disk_quota_mb: int

    def as_dict(self) -> dict:
        return asdict(self)

    def cpu_cores(self) -> float:
        return round(self.cpu_millicores / 1000, 3)

    def cpu_limit(self) -> str:
        return quota.format_cpu(self.cpu_millicores)

    def mem_limit(self) -> str:
        return quota.format_memory(self.memory_mb)


def _clamp(key: str, value: int) -> int:
    bounds = BOUNDS.get(key)
    if not bounds:
        return value
    low, high = bounds
    return max(low, min(high, value))


def _choice(value: str, choices: tuple[str, ...], fallback: str) -> str:
    cleaned = (value or "").strip().lower()
    return cleaned if cleaned in choices else fallback


def prefs_for(owner_uid: str) -> dict | None:
    if not owner_uid:
        return None
    return get_table(PREFS_TABLE).find_one(
        owner_kind="user", owner_id=owner_uid, deleted_at=None
    )


def _inherits(key: str, row: dict | None) -> bool:
    if not row:
        return True
    value = row.get(key)
    if value is None:
        return True
    if key in SENTINELS:
        return int(value) == SENTINELS[key]
    if isinstance(DEFAULTS[key], str):
        return not str(value).strip()
    return int(value) == INHERIT_INT


def _site_value(key: str):
    default = DEFAULTS[key]
    setting = SETTING_KEYS[key]
    if isinstance(default, bool):
        return get_setting(setting, "1" if default else "0") == "1"
    if isinstance(default, int):
        return get_int_setting(setting, default)
    return get_setting(setting, default)


def _normalize(key: str, value):
    default = DEFAULTS[key]
    if key in CHOICES:
        return _choice(value, CHOICES[key], default)
    if isinstance(default, bool):
        if isinstance(value, bool):
            return value
        try:
            return bool(int(value))
        except (TypeError, ValueError):
            return bool(value)
    if isinstance(default, int):
        try:
            return _clamp(key, int(value))
        except (TypeError, ValueError):
            return default
    return str(value)


def source_map(owner_uid: str = "") -> dict[str, str]:
    row = prefs_for(owner_uid)
    sources = {key: SOURCE_SITE for key in SETTING_KEYS}
    for key in PREF_COLUMNS:
        if not _inherits(key, row):
            sources[key] = SOURCE_USER
    return sources


def resolve(owner_uid: str = "", instance: dict | None = None) -> EditorProfile:
    row = prefs_for(owner_uid)
    values = {}
    for key in SETTING_KEYS:
        value = _site_value(key)
        if key in PREF_COLUMNS and not _inherits(key, row):
            value = row.get(key)
        values[key] = _normalize(key, value)
    limits = quota.resolve(owner_uid, instance)
    return EditorProfile(
        cpu_millicores=limits.cpu_millicores,
        memory_mb=limits.memory_mb,
        disk_quota_mb=limits.disk_quota_mb,
        **values,
    )


def settings_for(profile: EditorProfile) -> dict:
    settings = {
        "editor.fontSize": profile.font_size,
        "terminal.integrated.fontSize": profile.terminal_font_size,
        "window.zoomLevel": profile.zoom_level,
        "telemetry.telemetryLevel": "off",
        "update.mode": "none",
        "workbench.tips.enabled": False,
        "workbench.startupEditor": "none",
        "extensions.autoCheckUpdates": False,
        **FOREIGN_AI_SETTINGS,
        "terminal.integrated.defaultProfile.linux": "bash",
        "terminal.integrated.profiles.linux": {
            "bash": {"path": "/bin/bash", "args": ["-l"], "icon": "terminal-bash"},
            "DevPlace Code": {"path": "/usr/bin/dpc", "icon": "rocket"},
        },
    }
    settings.update(LAYOUTS[profile.layout])
    if profile.theme in THEME_LABELS:
        settings["workbench.colorTheme"] = THEME_LABELS[profile.theme]
    if profile.trust_all:
        settings.update(TRUST_SETTINGS)
    return settings


def merge_managed(current: dict, managed: dict, desired: dict) -> tuple[dict, dict]:
    merged = dict(current)
    for key, value in desired.items():
        if key not in merged or merged[key] == managed.get(key):
            merged[key] = value
    return merged, dict(desired)


def _read_json(path: Path) -> dict:
    try:
        data = json.loads(path.read_text())
    except (OSError, ValueError):
        return {}
    return data if isinstance(data, dict) else {}


def state_dir(instance: dict) -> Path:
    return config.WORKSPACE_STATE_DIR / instance["uid"]


def profile_payload(instance: dict, profile: EditorProfile) -> dict:
    return {
        "app_name": APP_NAME,
        "workspace_uid": instance.get("uid", ""),
        "boot_marker": instance.get("boot_marker", ""),
        "editor": profile.as_dict(),
    }


def seed_state(instance: dict, profile: EditorProfile) -> bool:
    root = state_dir(instance)
    user_dir = root / "data" / "User"
    try:
        user_dir.mkdir(parents=True, exist_ok=True)
        settings_path = user_dir / "settings.json"
        managed_path = user_dir / MANAGED_FILE
        settings, managed = merge_managed(
            _read_json(settings_path),
            _read_json(managed_path),
            settings_for(profile),
        )
        settings_path.write_text(json.dumps(settings, indent=2, sort_keys=True))
        managed_path.write_text(json.dumps(managed, indent=2, sort_keys=True))
        (root / PROFILE_FILE).write_text(
            json.dumps(profile_payload(instance, profile), indent=2, sort_keys=True)
        )
    except OSError as error:
        logger.warning("workspace editor seed failed for %s: %s", instance.get("uid"), error)
        return False
    return True


def argv(instance: dict, profile: EditorProfile) -> list[str]:
    port = int(instance.get("editor_port") or EDITOR_DEFAULT_PORT)
    command = [
        "code-server",
        "--bind-addr",
        f"0.0.0.0:{port}",
        "--auth",
        "password",
        "--app-name",
        APP_NAME,
        "--welcome-text",
        WELCOME_TEXT,
        "--disable-telemetry",
        "--disable-update-check",
        "--disable-getting-started-override",
    ]
    if profile.trust_all:
        command.append("--disable-workspace-trust")
    command += [
        "--user-data-dir",
        f"{WORKSPACE_STATE_MOUNT}/data",
        "--extensions-dir",
        f"{WORKSPACE_STATE_MOUNT}/extensions",
        WORKSPACE_MOUNT,
    ]
    return command


ENV_EXPORT_FILE = "/etc/profile.d/devplace-env.sh"

_ENV_EXPORT_SCRIPT = (
    "import os, pathlib, shlex\n"
    f"path = pathlib.Path({ENV_EXPORT_FILE!r})\n"
    "lines = ['export ' + k + '=' + shlex.quote(v) for k, v in sorted(os.environ.items()) if k.startswith('DEVPLACE_')]\n"
    "path.write_text('\\n'.join(lines) + '\\n' if lines else '')\n"
)


def wrap_with_env_export(command: list[str]) -> list[str]:
    export_step = f"umask 022; python3 -c {shlex.quote(_ENV_EXPORT_SCRIPT)} 2>/dev/null || true"
    script = f"{export_step}; exec {shlex.join(command)}"
    return ["/bin/sh", "-c", script]


def env_for(profile: EditorProfile) -> dict:
    return {
        "DEVPLACE_EDITOR_APP_NAME": APP_NAME,
        "DEVPLACE_EDITOR_PROFILE": f"{WORKSPACE_STATE_MOUNT}/{PROFILE_FILE}",
        "DEVPLACE_EDITOR_THEME": profile.theme,
        "DEVPLACE_EDITOR_FONT_SIZE": str(profile.font_size),
        "DEVPLACE_EDITOR_TERMINAL_FONT_SIZE": str(profile.terminal_font_size),
        "DEVPLACE_EDITOR_ZOOM_LEVEL": str(profile.zoom_level),
        "DEVPLACE_EDITOR_LAYOUT": profile.layout,
        "DEVPLACE_EDITOR_PANEL_PRESET": profile.panel_preset,
        "DEVPLACE_EDITOR_BOOT_AGENT": profile.boot_agent,
        "DEVPLACE_EDITOR_BOOT_SHELL": "1" if profile.boot_shell else "0",
        "DEVPLACE_EDITOR_TRUST_ALL": "1" if profile.trust_all else "0",
    }


def booted_profile(instance: dict) -> dict:
    return _read_json(state_dir(instance) / PROFILE_FILE).get("editor") or {}


def restart_required(instance: dict, profile: EditorProfile) -> bool:
    booted = booted_profile(instance)
    if not booted:
        return False
    return booted != profile.as_dict()


def save_prefs(owner_uid: str, payload: dict) -> dict:
    from devplacepy.utils import generate_uid

    table = get_table(PREFS_TABLE)
    row = prefs_for(owner_uid)
    values = {key: payload[key] for key in PREF_COLUMNS if key in payload}
    stamp = store.now()
    if row:
        table.update({"uid": row["uid"], "updated_at": stamp, **values}, ["uid"])
        return table.find_one(uid=row["uid"])
    uid = generate_uid()
    table.insert(
        {
            "uid": uid,
            "owner_kind": "user",
            "owner_id": owner_uid,
            "created_at": stamp,
            "updated_at": stamp,
            "deleted_at": None,
            "deleted_by": None,
            **{key: _blank(key) for key in PREF_COLUMNS},
            **values,
        }
    )
    return table.find_one(uid=uid)


def reset_prefs(owner_uid: str, actor_uid: str) -> bool:
    row = prefs_for(owner_uid)
    if not row:
        return False
    get_table(PREFS_TABLE).update(
        {"uid": row["uid"], "deleted_at": store.now(), "deleted_by": actor_uid}, ["uid"]
    )
    return True


def _blank(key: str):
    if key in SENTINELS:
        return SENTINELS[key]
    if isinstance(DEFAULTS[key], str):
        return INHERIT_TEXT
    return INHERIT_INT


def view(owner_uid: str = "", instance: dict | None = None) -> dict:
    profile = resolve(owner_uid, instance)
    payload = profile.as_dict()
    payload["cpu_cores"] = profile.cpu_cores()
    payload["sources"] = source_map(owner_uid)
    return payload
