import json
import os
from typing import Any, Dict, List, Optional, Union

import requests

from mnemosynecore.secrets import resolve_secret, resolve_secret_test

LlmConn = Union[str, Dict[str, Any]]


def _parse_extra(extra: Any) -> Dict[str, Any]:
    if isinstance(extra, dict):
        return dict(extra)
    if isinstance(extra, str) and extra.strip():
        try:
            parsed = json.loads(extra)
            if isinstance(parsed, dict):
                return parsed
        except Exception:
            pass
    return {}


def _resolve_llm_conn(
    llm_conn: Optional[LlmConn],
    *,
    test_mode: bool,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Dict[str, Any]:
    if llm_conn is None:
        return {}

    cfg: Dict[str, Any]
    if isinstance(llm_conn, dict):
        cfg = dict(llm_conn)
    elif isinstance(llm_conn, str):
        raw = llm_conn.strip()
        if not raw:
            return {}
        if raw.startswith("{"):
            cfg = json.loads(raw)
            if not isinstance(cfg, dict):
                raise ValueError("llm_conn JSON должен быть объектом")
        else:
            if test_mode:
                cfg = resolve_secret_test(raw, dir_path=dir_path, creds_path=creds_path)
            else:
                cfg = resolve_secret(raw)
    else:
        raise TypeError("llm_conn должен быть dict, str или None")

    cfg = dict(cfg)
    cfg["extra"] = _parse_extra(cfg.get("extra"))
    return cfg


def _read_token_from_path(token_path: str, token_field: str = "token") -> str:
    with open(token_path, "r", encoding="utf-8") as f:
        raw = f.read().strip()

    if not raw:
        raise ValueError(f"Файл токена пустой: {token_path}")

    try:
        parsed = json.loads(raw)
    except json.JSONDecodeError:
        return raw

    if isinstance(parsed, dict):
        for key in (
            token_field,
            "token",
            "api_key",
            "password",
            "key",
            "llm_token",
        ):
            if key and parsed.get(key):
                return str(parsed[key]).strip()
        raise ValueError(
            f"В JSON-файле {token_path} не найден токен "
            f"(поля: {token_field}, token, api_key, password, key, llm_token)"
        )

    if isinstance(parsed, str) and parsed.strip():
        return parsed.strip()

    raise ValueError(f"Не удалось прочитать токен из файла: {token_path}")


def _resolve_token(
    *,
    token: Optional[str],
    token_path: Optional[str],
    cfg: Dict[str, Any],
    token_field: str,
) -> str:
    if token and str(token).strip():
        return str(token).strip()

    if token_path:
        return _read_token_from_path(token_path, token_field=token_field)

    extra = _parse_extra(cfg.get("extra"))
    for key in (
        token_field,
        "token",
        "api_key",
        "password",
        "key",
        "llm_token",
    ):
        if key and cfg.get(key):
            return str(cfg[key]).strip()
        if key and extra.get(key):
            return str(extra[key]).strip()

    for env_name in ("LLM_API_KEY", "OPENAI_API_KEY"):
        env_value = os.getenv(env_name)
        if env_value and env_value.strip():
            return env_value.strip()

    raise ValueError(
        "Токен LLM не найден. Передайте token, token_path, llm_conn с token/api_key/password "
        "или задайте LLM_API_KEY/OPENAI_API_KEY"
    )


def _resolve_model(model: Optional[str], cfg: Dict[str, Any]) -> str:
    if model and model.strip():
        return model.strip()

    extra = _parse_extra(cfg.get("extra"))
    for key in ("model",):
        if cfg.get(key):
            return str(cfg[key]).strip()
        if extra.get(key):
            return str(extra[key]).strip()

    env_model = os.getenv("LLM_MODEL")
    if env_model and env_model.strip():
        return env_model.strip()

    raise ValueError("Модель LLM не указана. Передайте model или добавьте model в llm_conn/LLM_MODEL")


def _build_llm_url(
    *,
    base_url: Optional[str],
    endpoint: Optional[str],
    cfg: Dict[str, Any],
) -> str:
    extra = _parse_extra(cfg.get("extra"))
    resolved_base_url = (
        (base_url or "").strip()
        or str(cfg.get("base_url") or "").strip()
        or str(cfg.get("host") or "").strip()
        or str(extra.get("base_url") or "").strip()
        or str(extra.get("host") or "").strip()
        or os.getenv("LLM_BASE_URL", "").strip()
        or "https://api.openai.com/v1"
    )

    resolved_endpoint = (
        (endpoint or "").strip()
        or str(cfg.get("endpoint") or "").strip()
        or str(extra.get("endpoint") or "").strip()
        or os.getenv("LLM_ENDPOINT", "").strip()
        or "/chat/completions"
    )

    if resolved_endpoint.startswith("http://") or resolved_endpoint.startswith("https://"):
        return resolved_endpoint

    if not resolved_endpoint.startswith("/"):
        resolved_endpoint = "/" + resolved_endpoint

    return resolved_base_url.rstrip("/") + resolved_endpoint


def _build_messages(
    *,
    prompt: Optional[str],
    system_prompt: Optional[str],
    messages: Optional[List[Dict[str, Any]]],
) -> List[Dict[str, Any]]:
    if messages is not None:
        if not isinstance(messages, list):
            raise TypeError("messages должен быть списком словарей")
        final_messages = list(messages)
        if prompt and prompt.strip():
            final_messages.append({"role": "user", "content": prompt})
        if not final_messages:
            raise ValueError("messages пустой")
        return final_messages

    if not prompt or not prompt.strip():
        raise ValueError("Нужно передать prompt или messages")

    final_messages: List[Dict[str, Any]] = []
    if system_prompt and system_prompt.strip():
        final_messages.append({"role": "system", "content": system_prompt})
    final_messages.append({"role": "user", "content": prompt})
    return final_messages


def _extract_text_from_response(data: Dict[str, Any]) -> str:
    output_text = data.get("output_text")
    if isinstance(output_text, str) and output_text.strip():
        return output_text.strip()
    if isinstance(output_text, list):
        joined = "".join(str(x) for x in output_text if x is not None).strip()
        if joined:
            return joined

    choices = data.get("choices")
    if not isinstance(choices, list) or not choices:
        raise ValueError("LLM не вернул choices/output_text")

    first_choice = choices[0]
    if not isinstance(first_choice, dict):
        raise ValueError("Некорректный формат choices[0]")

    if isinstance(first_choice.get("text"), str) and first_choice["text"].strip():
        return first_choice["text"].strip()

    message = first_choice.get("message")
    if not isinstance(message, dict):
        raise ValueError("LLM не вернул message в choices[0]")

    content = message.get("content")
    if isinstance(content, str):
        return content.strip()

    if isinstance(content, list):
        parts: List[str] = []
        for item in content:
            if isinstance(item, str):
                parts.append(item)
                continue
            if isinstance(item, dict):
                text = item.get("text") or item.get("content")
                if text:
                    parts.append(str(text))
        joined = "".join(parts).strip()
        if joined:
            return joined

    raise ValueError("Не удалось извлечь текст из ответа LLM")


def _llm_chat_with_cfg(
    *,
    prompt: Optional[str],
    model: Optional[str],
    llm_cfg: Dict[str, Any],
    token: Optional[str],
    token_path: Optional[str],
    token_field: str,
    base_url: Optional[str],
    endpoint: Optional[str],
    system_prompt: Optional[str],
    messages: Optional[List[Dict[str, Any]]],
    temperature: Optional[float],
    max_tokens: Optional[int],
    timeout_sec: int,
    extra_body: Optional[Dict[str, Any]],
    headers: Optional[Dict[str, str]],
    parse_json: bool,
    return_raw: bool,
) -> Any:
    final_token = _resolve_token(
        token=token,
        token_path=token_path,
        cfg=llm_cfg,
        token_field=token_field,
    )
    final_model = _resolve_model(model=model, cfg=llm_cfg)
    final_url = _build_llm_url(base_url=base_url, endpoint=endpoint, cfg=llm_cfg)
    final_messages = _build_messages(
        prompt=prompt,
        system_prompt=system_prompt,
        messages=messages,
    )

    payload: Dict[str, Any] = {
        "model": final_model,
        "messages": final_messages,
    }
    if temperature is not None:
        payload["temperature"] = float(temperature)
    if max_tokens is not None:
        payload["max_tokens"] = int(max_tokens)
    if extra_body:
        payload.update(extra_body)

    request_headers: Dict[str, str] = {
        "Authorization": f"Bearer {final_token}",
        "Content-Type": "application/json",
    }
    if headers:
        request_headers.update(headers)

    resp = requests.post(
        final_url,
        json=payload,
        headers=request_headers,
        timeout=timeout_sec,
    )
    resp.raise_for_status()
    raw_data = resp.json()

    if return_raw:
        return raw_data

    text = _extract_text_from_response(raw_data)
    if not parse_json:
        return text

    try:
        return json.loads(text)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Ответ LLM не является корректным JSON: {exc}") from exc


def llm_chat(
    *,
    prompt: Optional[str] = None,
    model: Optional[str] = None,
    llm_conn: Optional[LlmConn] = None,
    token: Optional[str] = None,
    token_path: Optional[str] = None,
    token_field: str = "token",
    base_url: Optional[str] = None,
    endpoint: Optional[str] = None,
    system_prompt: Optional[str] = None,
    messages: Optional[List[Dict[str, Any]]] = None,
    temperature: Optional[float] = 0.0,
    max_tokens: Optional[int] = None,
    timeout_sec: int = 60,
    extra_body: Optional[Dict[str, Any]] = None,
    headers: Optional[Dict[str, str]] = None,
    parse_json: bool = False,
    return_raw: bool = False,
) -> Any:
    llm_cfg = _resolve_llm_conn(llm_conn, test_mode=False)
    return _llm_chat_with_cfg(
        prompt=prompt,
        model=model,
        llm_cfg=llm_cfg,
        token=token,
        token_path=token_path,
        token_field=token_field,
        base_url=base_url,
        endpoint=endpoint,
        system_prompt=system_prompt,
        messages=messages,
        temperature=temperature,
        max_tokens=max_tokens,
        timeout_sec=timeout_sec,
        extra_body=extra_body,
        headers=headers,
        parse_json=parse_json,
        return_raw=return_raw,
    )


def llm_chat_test(
    *,
    prompt: Optional[str] = None,
    model: Optional[str] = None,
    llm_conn: Optional[LlmConn] = None,
    token: Optional[str] = None,
    token_path: Optional[str] = None,
    token_field: str = "token",
    base_url: Optional[str] = None,
    endpoint: Optional[str] = None,
    system_prompt: Optional[str] = None,
    messages: Optional[List[Dict[str, Any]]] = None,
    temperature: Optional[float] = 0.0,
    max_tokens: Optional[int] = None,
    timeout_sec: int = 60,
    extra_body: Optional[Dict[str, Any]] = None,
    headers: Optional[Dict[str, str]] = None,
    parse_json: bool = False,
    return_raw: bool = False,
    dir_path: Optional[str] = None,
    creds_path: Optional[str] = None,
) -> Any:
    llm_cfg = _resolve_llm_conn(
        llm_conn,
        test_mode=True,
        dir_path=dir_path,
        creds_path=creds_path,
    )
    return _llm_chat_with_cfg(
        prompt=prompt,
        model=model,
        llm_cfg=llm_cfg,
        token=token,
        token_path=token_path,
        token_field=token_field,
        base_url=base_url,
        endpoint=endpoint,
        system_prompt=system_prompt,
        messages=messages,
        temperature=temperature,
        max_tokens=max_tokens,
        timeout_sec=timeout_sec,
        extra_body=extra_body,
        headers=headers,
        parse_json=parse_json,
        return_raw=return_raw,
    )
