"""Reasoners domain models.

These dataclasses represent the stable public shapes returned by the reasoners service
(both SQL-app and Direct Access backends). Gateways are responsible for translating
backend-specific payloads/rows into these models.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Mapping

from . import constants as c


class ReasonerType:
    """Reasoner type helpers (normalization + user-friendly labels/descriptions)."""

    LOGIC = c.REASONER_TYPE_LOGIC
    PRESCRIPTIVE = c.REASONER_TYPE_PRESCRIPTIVE
    PREDICTIVE = c.REASONER_TYPE_PREDICTIVE

    _LABELS = c.REASONER_TYPE_LABELS
    _DESCRIPTIONS = c.REASONER_TYPE_DESCRIPTIONS

    @classmethod
    def normalize(cls, value: str | None) -> str:
        """Normalize a type string to the canonical uppercase representation.

        Accepted values (case-insensitive):
        - Logic
        - Prescriptive
        - Predictive
        """
        v = (value or "").strip()
        if not v:
            return c.DEFAULT_REASONER_TYPE
        u = v.upper()
        if u in (cls.LOGIC, cls.PRESCRIPTIVE, cls.PREDICTIVE):
            return u
        # Also accept user-facing labels (e.g. "Logic") as input.
        label_map = {cls.get_label(t).upper(): t for t in (cls.LOGIC, cls.PRESCRIPTIVE, cls.PREDICTIVE)}
        if u in label_map:
            return label_map[u]
        raise ValueError(f"Invalid reasoner type {value!r}. Expected: Logic, Prescriptive, or Predictive.")

    @classmethod
    def get_label(cls, value: str) -> str:
        """Return a friendly label for a reasoner type."""
        return cls._LABELS.get(value, value)

    @classmethod
    def get_description(cls, value: str) -> str:
        """Return a friendly description for a reasoner type."""
        return cls._DESCRIPTIONS.get(value, "Unknown reasoner type")


@dataclass(frozen=True)
class Reasoner:
    """Reasoner instance metadata."""

    name: str
    type: str
    id: str | None = None
    size: str | None = None
    state: str | None = None
    created_by: str | None = None
    created_on: datetime | None = None
    updated_on: datetime | None = None
    version: str | None = None
    auto_suspend_mins: int | None = None
    suspends_at: datetime | None = None
    settings: dict[str, Any] | None = None

    def __repr__(self) -> str:
        return f"Reasoner(name={self.name!r}, type={self.type!r}, state={self.state!r})"


class OperationStatus:
    """Normalized operation status values + helpers."""

    PENDING = c.STATUS_PENDING
    RUNNING = c.STATUS_RUNNING
    SUCCEEDED = c.STATUS_SUCCEEDED
    FAILED = c.STATUS_FAILED
    CANCELLED = c.STATUS_CANCELLED

    @classmethod
    def done(cls, status: str | None) -> bool:
        """Return True if a status is terminal (succeeded/failed/cancelled)."""
        return (status or "").upper() in c.TERMINAL_STATUSES


@dataclass(frozen=True)
class Operation:
    """Data-only operation status record.

    This is the backend-facing model returned by gateways and `get_operation(...)` /
    `wait_for_operation(...)`. It intentionally contains no polling behavior.

    For client-side operation handles (with `wait()` / `result()` that return READY
    `Reasoner` objects), see `ReasonerOperation` / `ReasonerOperationSync` in
    `relationalai.services.reasoners.operations`.
    """

    id: str
    status: str | None = None
    created_on: datetime | None = None
    updated_on: datetime | None = None
    error: str | None = None
    # Raw payload for forward-compatibility across backends
    raw: dict[str, Any] | None = None

    def done(self) -> bool:
        """Return True if this operation is in a terminal state."""
        return OperationStatus.done(self.status)


def parse_settings(val: Any) -> dict[str, Any] | None:
    """Parse SETTINGS column from Snowflake (variant/str/dict) to a dict."""
    if val is None:
        return None
    if isinstance(val, dict):
        return val
    if isinstance(val, str):
        s = val.strip()
        if not s:
            return None
        try:
            parsed = json.loads(s)
            return parsed if isinstance(parsed, dict) else {"value": parsed}
        except Exception:
            return {"value": val}
    # preserve other types under wrapper
    return {"value": val}


def reasoner_from_row(row: Mapping[str, Any]) -> Reasoner:
    """Build a `Reasoner` from a Snowpark row-like mapping.

    Args:
        row: A mapping with at least `NAME` and `TYPE` keys (Snowflake columns).

    Returns:
        The normalized `Reasoner` model.

    Raises:
        ValueError: If required keys are missing.
    """
    # Snowpark Row supports dict-style indexing; for mapping we assume keys exist.
    name = row.get("NAME")
    rtype = row.get("TYPE")
    if name is None or rtype is None:
        raise ValueError(f"Invalid reasoner row (missing NAME/TYPE): {dict(row)}")
    return Reasoner(
        name=str(name),
        type=str(rtype),
        id=row.get("ID"),
        size=row.get("SIZE"),
        state=row.get("STATUS"),
        created_by=row.get("CREATED_BY"),
        created_on=row.get("CREATED_ON"),
        updated_on=row.get("UPDATED_ON"),
        version=row.get("VERSION"),
        auto_suspend_mins=row.get("AUTO_SUSPEND_MINS") or row.get("AUTO_SUSPEND"),
        suspends_at=row.get("SUSPENDS_AT"),
        settings=parse_settings(row.get("SETTINGS")),
    )

