# retoor <retoor@molodetz.nl>

import asyncio
import json
import logging
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime, timedelta, timezone

from devplacepy.database import db, get_table, get_setting, get_int_setting

logger = logging.getLogger(__name__)


class ConfigField:
    def __init__(
        self,
        key,
        label,
        type="str",
        default="",
        help="",
        options=None,
        secret=False,
        minimum=None,
        maximum=None,
        group="General",
    ):
        self.key = key
        self.label = label
        self.type = type
        self.default = default
        self.help = help
        self.options = options or []
        self.secret = secret
        self.minimum = minimum
        self.maximum = maximum
        self.group = group

    def coerce(self, raw):
        if self.type == "int":
            try:
                value = int(str(raw).strip())
            except (TypeError, ValueError):
                raise ValueError(f"{self.label} must be a whole number")
            if self.minimum is not None and value < self.minimum:
                raise ValueError(f"{self.label} must be at least {self.minimum}")
            if self.maximum is not None and value > self.maximum:
                raise ValueError(f"{self.label} must be at most {self.maximum}")
            return value
        if self.type == "float":
            try:
                value = float(str(raw).strip())
            except (TypeError, ValueError):
                raise ValueError(f"{self.label} must be a number")
            if self.minimum is not None and value < self.minimum:
                raise ValueError(f"{self.label} must be at least {self.minimum}")
            if self.maximum is not None and value > self.maximum:
                raise ValueError(f"{self.label} must be at most {self.maximum}")
            return value
        if self.type == "bool":
            text = str(raw).strip()
            if text in ("0", "1"):
                return text == "1"
            raise ValueError(f"{self.label} must be enabled or disabled")
        if self.type == "select":
            allowed = [option["value"] for option in self.options]
            if raw not in allowed:
                raise ValueError(f"{self.label} has an invalid selection")
            return raw
        if self.type == "url":
            text = str(raw).strip()
            if text and not (text.startswith("http://") or text.startswith("https://")):
                raise ValueError(f"{self.label} must be a http(s) URL")
            return text
        return str(raw)

    def to_storage(self, value):
        if isinstance(value, bool):
            return "1" if value else "0"
        return str(value)

    def read(self):
        raw = get_setting(self.key, "")
        if raw == "":
            return self.default
        try:
            return self.coerce(raw)
        except ValueError:
            return self.default

    def display_value(self):
        if self.secret:
            return ""
        raw = get_setting(self.key, "")
        if raw == "":
            return self.to_storage(self.default)
        return raw

    def spec(self):
        return {
            "key": self.key,
            "label": self.label,
            "type": self.type,
            "help": self.help,
            "options": self.options,
            "secret": self.secret,
            "minimum": self.minimum,
            "maximum": self.maximum,
            "value": self.display_value(),
            "is_set": bool(get_setting(self.key, "")) if self.secret else None,
            "group": self.group,
        }


class BaseService(ABC):
    config_fields = []
    interval_key = None
    min_interval = 1
    default_enabled = True
    title = ""
    description = ""
    details = ""
    DEFAULT_LOG_SIZE = 20
    TICK_SECONDS = 1
    PERSIST_SECONDS = 8
    METRICS_SECONDS = 15
    STALE_SECONDS = 15

    def __init__(self, name: str, interval_seconds: int = 3600):
        self.name = name
        self.interval_seconds = interval_seconds
        self.interval_key = self.interval_key or f"service_{name}_interval"
        self.enabled_key = f"service_{name}_enabled"
        self.command_key = f"service_{name}_command"
        self.log_size_key = f"service_{name}_log_size"
        self.log_buffer = deque(maxlen=self.DEFAULT_LOG_SIZE)
        self._task = None
        self._running = False
        self._shutdown = False
        self._started_at = None
        self._last_run = None
        self._next_run = None
        self._next_due = None
        self._run_pending = False
        self._last_command = None
        self._last_persist = None
        self._last_metrics = None
        self._metrics_snapshot = {}
        self._state_row_id = None
        self.enabled_field = ConfigField(
            self.enabled_key,
            "Enabled",
            type="bool",
            default=self.default_enabled,
            help="When enabled the service runs on its interval and starts on boot.",
            group="General",
        )
        self.interval_field = ConfigField(
            self.interval_key,
            "Run interval (seconds)",
            type="int",
            default=interval_seconds,
            minimum=self.min_interval,
            help=f"Delay between runs. Minimum {self.min_interval} seconds.",
            group="General",
        )
        self.log_size_field = ConfigField(
            self.log_size_key,
            "Log buffer size",
            type="int",
            default=self.DEFAULT_LOG_SIZE,
            minimum=1,
            maximum=200,
            help="Number of recent log lines to keep.",
            group="Advanced",
        )

    def all_fields(self) -> list:
        return [
            self.enabled_field,
            self.interval_field,
            *self.config_fields,
            self.log_size_field,
        ]

    def _grouped_fields(self) -> list:
        groups: dict = {}
        order: list = []
        for field in self.all_fields():
            spec = field.spec()
            group = spec["group"]
            if group not in groups:
                groups[group] = []
                order.append(group)
            groups[group].append(spec)
        return [{"name": name, "fields": groups[name]} for name in order]

    @property
    def status(self) -> str:
        return "running" if self._running else "stopped"

    def is_enabled(self) -> bool:
        return (
            get_setting(self.enabled_key, "1" if self.default_enabled else "0") == "1"
        )

    def current_interval(self) -> int:
        return max(
            self.min_interval, get_int_setting(self.interval_key, self.interval_seconds)
        )

    def get_config(self) -> dict:
        return {field.key: field.read() for field in self.all_fields()}

    def log(self, message: str) -> None:
        stamp = datetime.now(timezone.utc).strftime("%H:%M:%S")
        self.log_buffer.append(f"[{stamp}] {message}")
        logger.info(f"[{self.name}] {message}")

    @abstractmethod
    async def run_once(self) -> None:
        pass

    async def on_enable(self) -> None:
        pass

    async def on_disable(self) -> None:
        pass

    def collect_metrics(self) -> dict:
        return {}

    def _safe_metrics(self) -> dict:
        try:
            return self.collect_metrics()
        except Exception as e:
            logger.warning(f"Could not collect metrics for {self.name}: {e}")
            return {}

    def _current_metrics(self, now, force: bool = False) -> dict:
        if not force and self._last_metrics is not None:
            if (now - self._last_metrics).total_seconds() < self.METRICS_SECONDS:
                return self._metrics_snapshot
        self._last_metrics = now
        self._metrics_snapshot = self._safe_metrics()
        return self._metrics_snapshot

    def start_supervisor(self) -> None:
        if self._task is not None:
            return
        self._shutdown = False
        self._task = asyncio.create_task(self._run_loop())

    def request_shutdown(self) -> None:
        self._shutdown = True

    async def _run_loop(self) -> None:
        self._last_command = get_setting(self.command_key, "")
        self.log("Supervisor started")
        self._persist_state(force=True)
        while not self._shutdown:
            try:
                await self._tick()
            except asyncio.CancelledError:
                break
            except Exception as e:
                self.log(f"Loop error: {e}")
            try:
                await asyncio.sleep(self.TICK_SECONDS)
            except asyncio.CancelledError:
                break
        if self._started_at is not None:
            await self._safe_disable()
        self._running = False
        self._started_at = None
        self._persist_state(force=True)
        self.log("Supervisor stopped")

    async def _tick(self) -> None:
        self._sync_log_size()
        self._handle_commands()
        if self.is_enabled():
            if self._started_at is None:
                self._started_at = datetime.now(timezone.utc)
                self._running = True
                self.log("Service enabled")
                await self.on_enable()
            due = self._next_due is None or datetime.now(timezone.utc) >= self._next_due
            if self._run_pending or due:
                self._run_pending = False
                await self._execute_run()
        elif self._started_at is not None:
            await self._safe_disable()
            self._started_at = None
            self._running = False
            self._next_due = None
            self._next_run = None
            self.log("Service disabled")
        self._persist_state()

    async def _safe_disable(self) -> None:
        try:
            await self.on_disable()
        except Exception as e:
            self.log(f"on_disable error: {e}")

    async def _execute_run(self) -> None:
        self.interval_seconds = self.current_interval()
        self._last_run = datetime.now(timezone.utc).isoformat()
        self._next_run = None
        self._persist_state(force=True)
        try:
            await self.run_once()
        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.log(f"Error in run_once: {e}")
        self.interval_seconds = self.current_interval()
        self._next_due = datetime.now(timezone.utc) + timedelta(
            seconds=self.interval_seconds
        )
        self._next_run = self._next_due.isoformat()
        self.log(f"Next run in {self.interval_seconds}s")
        self._persist_state(force=True)

    def _handle_commands(self) -> None:
        raw = get_setting(self.command_key, "")
        if not raw or raw == self._last_command:
            return
        self._last_command = raw
        verb = raw.split(":", 1)[0]
        if verb == "run":
            self._run_pending = True
            self.log("Run requested")
        elif verb == "clear":
            self.log_buffer.clear()
            self.log("Logs cleared")

    def _sync_log_size(self) -> None:
        size = max(1, get_int_setting(self.log_size_key, self.DEFAULT_LOG_SIZE))
        if size != self.log_buffer.maxlen:
            self.log_buffer = deque(self.log_buffer, maxlen=size)

    def _persist_state(self, force: bool = False) -> None:
        now = datetime.now(timezone.utc)
        if not force and self._last_persist is not None:
            if (now - self._last_persist).total_seconds() < self.PERSIST_SECONDS:
                return
        self._last_persist = now
        record = {
            "name": self.name,
            "status": self.status,
            "last_run": self._last_run or "",
            "next_run": self._next_run or "",
            "started_at": self._started_at.isoformat() if self._started_at else "",
            "heartbeat": now.isoformat(),
            "logs": json.dumps(list(self.log_buffer)),
            "metrics": json.dumps(self._current_metrics(now, force=force)),
            "updated_at": now.isoformat(),
        }
        try:
            table = get_table("service_state")
            if self._state_row_id is None:
                existing = table.find_one(name=self.name)
                self._state_row_id = existing["id"] if existing else None
            if self._state_row_id is not None:
                table.update({**record, "id": self._state_row_id}, ["id"])
            else:
                self._state_row_id = table.insert(record)
        except Exception as e:
            logger.warning(f"Could not persist state for {self.name}: {e}")

    def _read_state(self) -> dict:
        if "service_state" not in db.tables:
            return {}
        row = db["service_state"].find_one(name=self.name)
        return row or {}

    def _display_status(self, enabled: bool, state: dict) -> str:
        if not enabled:
            return "stopped"
        heartbeat = state.get("heartbeat")
        if not heartbeat:
            return "stalled"
        try:
            beat = datetime.fromisoformat(heartbeat)
        except ValueError:
            return "stalled"
        if (datetime.now(timezone.utc) - beat).total_seconds() > self.STALE_SECONDS:
            return "stalled"
        return "running"

    def _uptime(self, state: dict, status: str) -> str | None:
        if status != "running":
            return None
        started = state.get("started_at")
        if not started:
            return None
        try:
            since = datetime.fromisoformat(started)
        except ValueError:
            return None
        return str(datetime.now(timezone.utc) - since).split(".")[0]

    def _logs(self, state: dict) -> list:
        raw = state.get("logs")
        if not raw:
            return []
        try:
            return json.loads(raw)
        except (ValueError, TypeError):
            return []

    def _metrics(self, state: dict) -> dict:
        raw = state.get("metrics")
        if not raw:
            return self._safe_metrics()
        try:
            return json.loads(raw)
        except (ValueError, TypeError):
            return self._safe_metrics()

    def describe(self) -> dict:
        state = self._read_state()
        enabled = self.is_enabled()
        status = self._display_status(enabled, state)
        return {
            "name": self.name,
            "title": self.title or self.name.capitalize(),
            "description": self.description,
            "details": self.details,
            "enabled": enabled,
            "status": status,
            "interval_seconds": self.current_interval(),
            "last_run": state.get("last_run") or None,
            "next_run": state.get("next_run") or None,
            "heartbeat": state.get("heartbeat") or None,
            "uptime": self._uptime(state, status),
            "log_buffer": self._logs(state),
            "metrics": self._metrics(state),
            "fields": [field.spec() for field in self.all_fields()],
            "field_groups": self._grouped_fields(),
        }
