"""
core/ai_engine.py — единая обёртка над любым OpenAI-совместимым API.

Юзер сам платит за токены. Поддерживаем:
  - OpenAI         (https://api.openai.com/v1)
  - OpenRouter     (https://openrouter.ai/api/v1) — дешевле всех, доступ к 100+ моделям
  - DeepSeek       (https://api.deepseek.com/v1) — почти бесплатно для русского
  - Groq           (https://api.groq.com/openai/v1) — быстро, бесплатный тариф
  - Anthropic      (через https://api.anthropic.com/v1, нативно)
  - Custom         любой URL который умеет /chat/completions

Никаких локальных LLM, GPU и установок — только HTTP-клиент к удалённому API.
Юзер вводит ключ → выбирает модель → бюджет → готово.

Использование:
    from core.ai_engine import generate_dialog_reply, generate_comment, ai_status
    # В прогреве:
    text = await generate_dialog_reply(persona, history, topic)
    # В коммент-модуле:
    text = await generate_comment(persona, post_text, channel_name, cta=None)
"""
import asyncio
import json
import logging
import time
from pathlib import Path
from typing import List, Dict, Optional, Tuple

logger = logging.getLogger("telepush.ai")


# ── Каталог провайдеров — для UI и тестов ──────────────────────────────

PROVIDERS = {
    "openai": {
        "label": "OpenAI",
        "url": "https://api.openai.com/v1",
        "signup": "https://platform.openai.com/api-keys",
        "models": ["gpt-4o-mini", "gpt-4o", "gpt-4.1-mini", "gpt-3.5-turbo"],
        "default_model": "gpt-4o-mini",
        # $ за 1М токенов (input/output) — для прикидки бюджета на UI.
        "price_per_1m_in": 0.15,
        "price_per_1m_out": 0.60,
        "hint": "Самое надёжное качество. Получи ключ за 5 минут — пополни баланс на $5, хватит на 6+ месяцев прогрева.",
    },
    "openrouter": {
        "label": "OpenRouter",
        "url": "https://openrouter.ai/api/v1",
        "signup": "https://openrouter.ai/keys",
        "models": ["openai/gpt-4o-mini", "deepseek/deepseek-chat", "anthropic/claude-3.5-haiku",
                   "google/gemini-2.0-flash-001", "meta-llama/llama-3.3-70b-instruct"],
        "default_model": "deepseek/deepseek-chat",
        "price_per_1m_in": 0.14,
        "price_per_1m_out": 0.28,
        "hint": "Один ключ — доступ к 100+ моделям. DeepSeek через OpenRouter в 5-10 раз дешевле OpenAI и почти не уступает на русском.",
    },
    "deepseek": {
        "label": "DeepSeek",
        "url": "https://api.deepseek.com/v1",
        "signup": "https://platform.deepseek.com/api_keys",
        "models": ["deepseek-chat", "deepseek-reasoner"],
        "default_model": "deepseek-chat",
        "price_per_1m_in": 0.14,
        "price_per_1m_out": 0.28,
        "hint": "Самый дешёвый вариант. На русском пишет ровно. Регистрация по email — пополнение от $1.",
    },
    "groq": {
        "label": "Groq",
        "url": "https://api.groq.com/openai/v1",
        "signup": "https://console.groq.com/keys",
        "models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
        "default_model": "llama-3.3-70b-versatile",
        "price_per_1m_in": 0.59,
        "price_per_1m_out": 0.79,
        "hint": "Бесплатный тариф 30 запросов/мин. Очень быстро. Качество русского чуть слабее GPT.",
    },
    "custom": {
        "label": "Custom (свой URL)",
        "url": "",
        "signup": "",
        "models": [],
        "default_model": "",
        "price_per_1m_in": 0.0,
        "price_per_1m_out": 0.0,
        "hint": "Любой OpenAI-совместимый эндпоинт: локальный Ollama (http://localhost:11434/v1), LM Studio, vLLM-сервер или прокси.",
    },
}


# ── Стартовые промпты — юзер потом сам пишет под свою тематику ─────────

DIALOG_SYSTEM_PROMPT_RU = """Ты пишешь короткое сообщение в чат от имени обычного человека из СНГ.

Правила:
— Пиши максимально естественно, как живой человек: разговорная речь, иногда без больших букв, иногда опечатка, эмодзи 0-2 на сообщение (часто 0).
— Сообщение 1-2 коротких предложения, изредка 3. Никаких списков, оформления, заголовков.
— НЕ выдавай что ты ИИ, нейросеть, бот, ассистент. Если спросят — отвечай уклончиво («да я обычный чел», «не понял вопрос»).
— Не рекламируй ничего. Не предлагай встретиться. Не давай ссылки.
— Не повторяй слово в слово предыдущие сообщения, варьируй.

Твоя личность:
{persona}

Текущая тема разговора (если есть): {topic}
"""

COMMENT_SYSTEM_PROMPT_RU = """Ты пишешь комментарий под постом в Telegram-канале.

Правила:
— Один-два коротких предложения, как живой подписчик: согласие/несогласие/уточнение/шутка/личный опыт.
— Без шаблонных фраз вроде «отличный пост», «спасибо за информацию», «очень полезно».
— Без списков, заголовков, оформления.
— Эмодзи 0-1, чаще 0.
— Не упоминай что ты ИИ.
— Не пиши общее «согласен», ответь по сути ИМЕННО ЭТОГО ПОСТА — зацепись за конкретную деталь.

Твоя личность подписчика:
{persona}

{cta_block}"""


# ── HTTP-клиент ────────────────────────────────────────────────────────

async def _post_chat(url: str, key: str, model: str, messages: List[Dict],
                      max_tokens: int = 300, temperature: float = 0.85,
                      timeout: float = 30.0) -> Tuple[Optional[str], Dict]:
    """Возвращает (text, usage_dict). text=None при ошибке."""
    import aiohttp
    headers = {
        "Authorization": f"Bearer {key}",
        "Content-Type": "application/json",
        "User-Agent": "TelePush/1.0",
    }
    body = {
        "model": model,
        "messages": messages,
        "max_tokens": max_tokens,
        "temperature": temperature,
    }
    full_url = url.rstrip("/") + "/chat/completions"
    try:
        async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as sess:
            async with sess.post(full_url, headers=headers, json=body) as r:
                data = await r.json()
                if r.status >= 400:
                    err = data.get("error", {}).get("message") if isinstance(data, dict) else str(data)
                    logger.warning("[ai] HTTP %d: %s", r.status, str(err)[:200])
                    return None, {"error": str(err)[:200], "status": r.status}
                text = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
                usage = data.get("usage", {}) or {}
                return text, usage
    except asyncio.TimeoutError:
        return None, {"error": "timeout", "status": 0}
    except Exception as e:
        return None, {"error": f"{type(e).__name__}: {e}"[:200], "status": 0}


# ── Бюджет — счётчик потраченного $ за день ────────────────────────────

class _Budget:
    """Простой in-memory счётчик. Перезагружается при рестарте — это OK,
    каждый день обнуляется в полночь."""

    def __init__(self):
        self.spent_usd_today: float = 0.0
        self.requests_today: int = 0
        self.day: str = ""

    def _maybe_rollover(self):
        from datetime import date
        today = str(date.today())
        if today != self.day:
            self.spent_usd_today = 0.0
            self.requests_today = 0
            self.day = today

    def add(self, in_tokens: int, out_tokens: int, prov: dict):
        self._maybe_rollover()
        usd = (in_tokens / 1_000_000) * prov.get("price_per_1m_in", 0.0) \
            + (out_tokens / 1_000_000) * prov.get("price_per_1m_out", 0.0)
        self.spent_usd_today += usd
        self.requests_today += 1
        return usd

    def check(self, daily_limit_usd: float, max_requests: int) -> Tuple[bool, str]:
        self._maybe_rollover()
        if daily_limit_usd > 0 and self.spent_usd_today >= daily_limit_usd:
            return False, f"дневной бюджет ${daily_limit_usd:.2f} исчерпан (потрачено ${self.spent_usd_today:.4f})"
        if max_requests > 0 and self.requests_today >= max_requests:
            return False, f"лимит {max_requests} запросов/день исчерпан"
        return True, ""


_BUDGET = _Budget()


# ── Загрузка настроек юзера ─────────────────────────────────────────────

def _load_user_ai_cfg() -> dict:
    """Читает настройки AI из user_settings.json (через web/app.py).
    Структура: {provider, url, api_key_enc, model, daily_limit_usd, max_requests, ...}.
    Возвращает {} если не настроено."""
    try:
        from config import DATA_DIR
        path = DATA_DIR / "user_settings.json"
        if not path.exists():
            return {}
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f) or {}
        return data.get("ai") or {}
    except Exception as e:
        logger.debug("[ai] load cfg: %s", e)
        return {}


def _decrypt_key(enc: str) -> str:
    if not enc:
        return ""
    try:
        from utils.secrets_storage import decrypt_secret
        return decrypt_secret(enc) or ""
    except Exception:
        return ""


# ── Публичный API ──────────────────────────────────────────────────────

def ai_status() -> dict:
    """Сводка для UI/диагностики: настроен ли AI, сколько потрачено сегодня."""
    cfg = _load_user_ai_cfg()
    enabled = bool(cfg.get("enabled")) and bool(cfg.get("api_key_enc")) and bool(cfg.get("provider"))
    _BUDGET._maybe_rollover()
    return {
        "enabled": enabled,
        "provider": cfg.get("provider", ""),
        "model": cfg.get("model", ""),
        "spent_usd_today": round(_BUDGET.spent_usd_today, 4),
        "requests_today": _BUDGET.requests_today,
        "daily_limit_usd": float(cfg.get("daily_limit_usd", 0) or 0),
        "max_requests": int(cfg.get("max_requests", 0) or 0),
    }


async def chat_complete(messages: List[Dict], *, max_tokens: int = 200,
                         temperature: float = 0.85) -> Optional[str]:
    """Низкоуровневый вызов: отправь messages → получи text. None при ошибке/бюджете.
    messages = [{"role":"system","content":"..."}, {"role":"user","content":"..."}]
    """
    cfg = _load_user_ai_cfg()
    if not cfg or not cfg.get("enabled") or not cfg.get("api_key_enc"):
        return None
    provider_id = cfg.get("provider") or "openai"
    prov = PROVIDERS.get(provider_id) or PROVIDERS["openai"]
    url = (cfg.get("url") or prov["url"]).strip()
    if not url:
        return None
    model = (cfg.get("model") or prov.get("default_model") or "gpt-4o-mini").strip()
    key = _decrypt_key(cfg.get("api_key_enc", ""))
    if not key:
        return None

    # Проверяем бюджет
    daily = float(cfg.get("daily_limit_usd", 0) or 0)
    max_req = int(cfg.get("max_requests", 0) or 0)
    ok, why = _BUDGET.check(daily, max_req)
    if not ok:
        logger.warning("[ai] %s — пропускаю запрос", why)
        return None

    text, usage = await _post_chat(url, key, model, messages,
                                    max_tokens=max_tokens, temperature=temperature)
    if text is None:
        logger.warning("[ai] запрос упал: %s", usage.get("error", "unknown"))
        return None

    in_t = int(usage.get("prompt_tokens", 0) or 0)
    out_t = int(usage.get("completion_tokens", 0) or 0)
    if in_t or out_t:
        usd = _BUDGET.add(in_t, out_t, prov)
        logger.debug("[ai] %s: in=%d out=%d $%.5f (today $%.4f)",
                      model, in_t, out_t, usd, _BUDGET.spent_usd_today)
    return text


async def generate_dialog_reply(persona: str, history: List[Dict],
                                  topic: str = "", *, custom_system: str = "") -> Optional[str]:
    """Генерирует одно сообщение для прогрева.
    persona: «Парень 28 лет, программист, любит играть в DotA, разговорный стиль».
    history: [{"role":"user","content":"..."}] или [{"role":"assistant","content":"..."}]
    topic: «обсуждение работы» / «выходные» / пусто = свободно.
    """
    sysprompt = (custom_system or DIALOG_SYSTEM_PROMPT_RU).format(
        persona=persona or "обычный пользователь Telegram",
        topic=topic or "свободная беседа",
    )
    msgs = [{"role": "system", "content": sysprompt}]
    msgs.extend(history[-10:])  # max 10 последних — экономим токены
    return await chat_complete(msgs, max_tokens=120, temperature=0.95)


async def generate_comment(persona: str, post_text: str, channel_name: str = "",
                             cta: str = "", *, custom_system: str = "") -> Optional[str]:
    """Генерирует комментарий под пост в канале."""
    cta_block = ""
    if cta:
        cta_block = (
            "Дополнительно (используй ТОЛЬКО если это органично вписывается в комментарий, "
            "иначе игнорируй): " + cta
        )
    sysprompt = (custom_system or COMMENT_SYSTEM_PROMPT_RU).format(
        persona=persona or "подписчик канала",
        cta_block=cta_block,
    )
    user_msg = f"Канал: @{channel_name}\nПост:\n---\n{post_text[:1500]}\n---\n\nНапиши свой комментарий."
    msgs = [
        {"role": "system", "content": sysprompt},
        {"role": "user", "content": user_msg},
    ]
    return await chat_complete(msgs, max_tokens=150, temperature=0.9)


async def test_connection(provider_id: str, url: str, key: str, model: str) -> dict:
    """Кнопка «Проверить ключ» в UI. Один запрос с короткой проверкой."""
    if not key:
        return {"ok": False, "error": "Введите API ключ"}
    prov = PROVIDERS.get(provider_id) or PROVIDERS["custom"]
    url = (url or prov.get("url") or "").strip()
    model = (model or prov.get("default_model") or "").strip()
    if not url or not model:
        return {"ok": False, "error": "Укажите URL и модель"}
    msgs = [
        {"role": "system", "content": "Ты помощник. Отвечай по-русски одним коротким предложением."},
        {"role": "user", "content": "Скажи коротко 'тест прошёл' и больше ничего."},
    ]
    text, usage = await _post_chat(url, key, model, msgs, max_tokens=20, temperature=0.0, timeout=15.0)
    if text is None:
        return {"ok": False, "error": usage.get("error", "Неизвестная ошибка")}
    in_t = int(usage.get("prompt_tokens", 0) or 0)
    out_t = int(usage.get("completion_tokens", 0) or 0)
    cost = (in_t / 1_000_000) * prov.get("price_per_1m_in", 0.0) \
         + (out_t / 1_000_000) * prov.get("price_per_1m_out", 0.0)
    return {
        "ok": True,
        "reply": text[:200],
        "tokens_in": in_t,
        "tokens_out": out_t,
        "cost_usd": round(cost, 6),
        "model": model,
    }
