"""Персистентный статус обучения модели для опроса из Laravel."""

from __future__ import annotations

import json
import logging
import threading
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

_STATUS_LOCK = threading.RLock()

DEFAULT_STATUS: Dict[str, Any] = {
    "run_id": None,
    "status": "idle",
    "progress": 0,
    "stage": "idle",
    "stage_label": "Ожидание",
    "started_at": None,
    "finished_at": None,
    "eta_seconds": None,
    "error": None,
    "error_code": None,
    "version_candidate": None,
    "version_activated": None,
    "activated": None,
    "gate": None,
    "records_used": None,
    "training_time_min": None,
    "message": None,
}


class TrainingStatusStore:
    """
    Хранит статус обучения в JSON-файле и в памяти процесса.

    Обеспечивает единый источник правды для GET /admin/train/status
    при создании ModelService на каждый HTTP-запрос.
    """

    def __init__(self, status_path: Path):
        self.status_path = Path(status_path)
        self.status_path.parent.mkdir(parents=True, exist_ok=True, mode=0o777)
        self._memory: Dict[str, Any] = self._load_from_disk()

    def _load_from_disk(self) -> Dict[str, Any]:
        if not self.status_path.exists():
            return dict(DEFAULT_STATUS)
        try:
            with open(self.status_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            merged = dict(DEFAULT_STATUS)
            merged.update(data)
            return merged
        except Exception as e:
            logger.warning("Failed to read training status file: %s", e)
            return dict(DEFAULT_STATUS)

    def _atomic_write(self, payload: Dict[str, Any]) -> None:
        tmp = self.status_path.with_suffix(".tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)
        tmp.replace(self.status_path)

    def get(self) -> Dict[str, Any]:
        with _STATUS_LOCK:
            self._memory = self._load_from_disk()
            return dict(self._memory)

    def update(self, **kwargs: Any) -> Dict[str, Any]:
        with _STATUS_LOCK:
            self._memory = self._load_from_disk()
            self._memory.update(kwargs)
            self._atomic_write(self._memory)
            return dict(self._memory)

    def begin_run(self, message: str = "Обучение запущено") -> Dict[str, Any]:
        """
        Создаёт новый run_id и переводит статус в running.

        :param message: Короткое сообщение для UI / webhook.
        :return: Актуальный статус.
        """
        return self.update(
            run_id=str(uuid.uuid4()),
            status="running",
            progress=0,
            stage="queued",
            stage_label="Подготовка",
            started_at=datetime.utcnow().isoformat(),
            finished_at=None,
            eta_seconds=None,
            error=None,
            error_code=None,
            version_candidate=None,
            version_activated=None,
            activated=None,
            gate=None,
            records_used=None,
            training_time_min=None,
            message=message,
        )

    def set_stage(
        self,
        stage: str,
        stage_label: str,
        progress: int,
        message: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Обновляет этап обучения.

        :param stage: Код этапа.
        :param stage_label: Человекочитаемое название.
        :param progress: Процент 0–100.
        :param message: Опциональное сообщение.
        """
        payload: Dict[str, Any] = {
            "status": "running",
            "stage": stage,
            "stage_label": stage_label,
            "progress": int(progress),
        }
        if message is not None:
            payload["message"] = message
        return self.update(**payload)

    def complete(
        self,
        *,
        activated: bool,
        version_candidate: Optional[int],
        version_activated: Optional[int],
        gate: Optional[Dict[str, Any]],
        records_used: Optional[int],
        training_time_min: Optional[float],
        message: str,
        rejected: bool = False,
    ) -> Dict[str, Any]:
        """
        Фиксирует успешное завершение обучения (в т.ч. gate-reject).

        :param activated: Была ли активирована новая версия.
        :param version_candidate: Номер кандидата.
        :param version_activated: Активная версия после train.
        :param gate: Результат сравнения метрик.
        :param records_used: Число строк обучения.
        :param training_time_min: Длительность в минутах.
        :param message: Короткий итог.
        :param rejected: True если gate отклонил активацию.
        """
        return self.update(
            status="rejected" if rejected else "completed",
            progress=100,
            stage="done",
            stage_label="Завершено",
            finished_at=datetime.utcnow().isoformat(),
            error=None,
            error_code=None,
            version_candidate=version_candidate,
            version_activated=version_activated,
            activated=activated,
            gate=gate,
            records_used=records_used,
            training_time_min=training_time_min,
            message=message,
        )

    def fail(self, error: str, error_code: str = "training_error") -> Dict[str, Any]:
        """
        Фиксирует ошибку обучения без stack trace в message.

        :param error: Краткое описание ошибки.
        :param error_code: Машинный код ошибки.
        """
        short = (error or "Неизвестная ошибка")[:240]
        return self.update(
            status="failed",
            progress=0,
            stage="failed",
            stage_label="Ошибка",
            finished_at=datetime.utcnow().isoformat(),
            error=short,
            error_code=error_code,
            message=f"Ошибка обучения: {short}",
            activated=False,
        )

    def mark_cancelling(self, message: str = "Отмена обучения запрошена") -> Dict[str, Any]:
        """Помечает прогон как cancelling (кооперативная остановка)."""
        return self.update(
            status="cancelling",
            stage="cancelling",
            stage_label="Отмена",
            message=message,
            error=None,
            error_code=None,
        )

    def cancel(self, message: str = "Обучение отменено") -> Dict[str, Any]:
        """
        Фиксирует отмену обучения (в т.ч. принудительный сброс зависшего статуса).

        :param message: Короткий итог для UI / webhook.
        """
        return self.update(
            status="cancelled",
            progress=0,
            stage="cancelled",
            stage_label="Отменено",
            finished_at=datetime.utcnow().isoformat(),
            error=None,
            error_code="cancelled",
            message=message,
            activated=False,
        )

    def is_running(self) -> bool:
        return self.get().get("status") in {"running", "cancelling"}
