from __future__ import annotations

from datetime import datetime, timedelta, timezone
import json
import os
import re
import shlex
import time

_MODEL_RESPONSE_CACHE: dict[str, str] = {}
_KNOWN_MODEL_PROVIDERS = {
    "openai",
    "anthropic",
    "google_genai",
    "google_vertexai",
    "openrouter",
    "ollama",
    "vllm",
    "lmstudio",
}

def _get(obj, key: str, default=None):
    if isinstance(obj, dict):
        return obj.get(key, default)
    return getattr(obj, key, default)


def _env_int(name: str, default: int, minimum: int = 1, maximum: int = 10) -> int:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        value = int(raw)
    except Exception:
        return default
    return max(minimum, min(value, maximum))


def _env_float(name: str, default: float, minimum: float = 0.1, maximum: float = 10.0) -> float:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        value = float(raw)
    except Exception:
        return default
    return max(minimum, min(value, maximum))


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name, "").strip().lower()
    if not raw:
        return default
    if raw in {"1", "true", "yes", "y", "on"}:
        return True
    if raw in {"0", "false", "no", "n", "off"}:
        return False
    return default


def _call_with_retry(label: str, fn, max_retries: int, base_delay_sec: float):
    errors: list[str] = []
    for attempt in range(1, max_retries + 1):
        try:
            return fn()
        except Exception as exc:
            errors.append(f"{attempt}회차: {exc}")
            if attempt == max_retries:
                break
            time.sleep(base_delay_sec * (2 ** (attempt - 1)))
    raise RuntimeError(f"{label} 실패 (재시도 {max_retries}회): {' | '.join(errors)}")


def _parse_dt(raw: str | None):
    if not raw:
        return None
    value = raw.strip()
    if not value:
        return None
    try:
        if len(value) == 10 and re.match(r"\d{4}-\d{2}-\d{2}$", value):
            dt = datetime.strptime(value, "%Y-%m-%d")
            return dt.replace(tzinfo=timezone.utc)
        normalized = value.replace("Z", "+00:00")
        dt = datetime.fromisoformat(normalized)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except Exception:
        return None


def _extract_json_block(message: str) -> dict:
    stripped = message.strip()
    if stripped.startswith("{") and stripped.endswith("}"):
        try:
            payload = json.loads(stripped)
            return payload if isinstance(payload, dict) else {}
        except Exception:
            return {}
    match = re.search(r"```json\s*(\{.*?\})\s*```", message, re.DOTALL)
    if not match:
        return {}
    try:
        payload = json.loads(match.group(1))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _parse_key_values(message: str) -> dict[str, str]:
    tokens: list[str] = []
    try:
        tokens = shlex.split(message)
    except Exception:
        tokens = message.split()

    pairs: dict[str, str] = {}
    for token in tokens:
        if "=" not in token:
            continue
        key, value = token.split("=", 1)
        key = key.strip()
        value = value.strip()
        if key:
            pairs[key] = value
    return pairs


def _parse_filters(message: str) -> dict:
    filters = {
        "mode": "overview",
        "from": None,
        "to": None,
        "limit": 20,
        "name": None,
        "user_id": None,
        "session_id": None,
        "trace_id": None,
        "tags": [],
        "environment": None,
        "metadata": {},
        "compare_models": False,
        "prompt": None,
    }

    payload = _extract_json_block(message)
    if payload:
        for key in ("mode", "from", "to", "name", "user_id", "session_id", "trace_id", "environment", "prompt"):
            if key in payload:
                filters[key] = payload.get(key)
        if "limit" in payload:
            try:
                filters["limit"] = int(payload.get("limit", 20))
            except Exception:
                filters["limit"] = 20
        tags = payload.get("tags", [])
        if isinstance(tags, str):
            tags = [t.strip() for t in tags.split(",") if t.strip()]
        if isinstance(tags, list):
            filters["tags"] = [str(t).strip() for t in tags if str(t).strip()]
        metadata = payload.get("metadata", {})
        if isinstance(metadata, dict):
            filters["metadata"] = {str(k): str(v) for k, v in metadata.items()}
        if "compare_models" in payload:
            filters["compare_models"] = bool(payload.get("compare_models"))
        return _normalize_filters(filters, message)

    kv = _parse_key_values(message)
    for key in ("mode", "from", "to", "name", "user_id", "session_id", "trace_id", "environment", "prompt"):
        if key in kv:
            filters[key] = kv[key]
    if "limit" in kv:
        try:
            filters["limit"] = int(kv["limit"])
        except Exception:
            filters["limit"] = 20
    if "tags" in kv:
        filters["tags"] = [v.strip() for v in kv["tags"].split(",") if v.strip()]
    for key, value in kv.items():
        if key.startswith("metadata."):
            filters["metadata"][key.split(".", 1)[1]] = value
    if "compare_models" in kv:
        filters["compare_models"] = kv["compare_models"].lower() in {"1", "true", "yes", "y"}
    return _normalize_filters(filters, message)


def _normalize_filters(filters: dict, message: str) -> dict:
    filters["limit"] = max(1, min(int(filters.get("limit", 20) or 20), 100))
    lowered = message.lower()
    mode = str(filters.get("mode", "") or "").strip().lower()
    if not mode:
        mode = _infer_mode_from_text(message)
    filters["mode"] = _normalize_mode(mode)
    if any(word in lowered for word in ("model compare", "compare models", "모델 비교", "모델별")):
        filters["compare_models"] = True
    return filters


def _normalize_mode(raw: str) -> str:
    aliases = {
        "overview": "overview",
        "summary": "overview",
        "observability": "observability",
        "debug": "observability",
        "root_cause": "observability",
        "prompt": "prompt_engineering",
        "prompt_engineering": "prompt_engineering",
        "eval": "evaluation",
        "evaluation": "evaluation",
    }
    return aliases.get(raw, "overview")


def compute_period_end(period_start: str, period_type: str) -> str:
    """Compute the period end date string from a start date and period type.

    The returned date is timezone-naive (date arithmetic only).
    """
    dt = datetime.strptime(period_start, "%Y-%m-%d")
    if period_type == "daily":
        return (dt + timedelta(days=1)).strftime("%Y-%m-%d")
    if period_type == "weekly":
        return (dt + timedelta(days=7)).strftime("%Y-%m-%d")
    if period_type == "monthly":
        if dt.month == 12:
            return dt.replace(year=dt.year + 1, month=1).strftime("%Y-%m-%d")
        return dt.replace(month=dt.month + 1).strftime("%Y-%m-%d")
    raise ValueError(f"Invalid period_type: {period_type}")


def _infer_mode_from_text(message: str) -> str:
    lowered = message.lower()
    if any(word in lowered for word in ("root cause", "debug", "관찰", "원인", "디버깅", "trace breakdown")):
        return "observability"
    if any(word in lowered for word in ("prompt", "프롬프트", "rewrite", "개선 문구")):
        return "prompt_engineering"
    if any(word in lowered for word in ("evaluation", "eval", "rubric", "평가", "채점")):
        return "evaluation"
    return "overview"

