from __future__ import annotations

import json
import re
import time
from pathlib import Path
from typing import Any

import httpx
from openai import OpenAI
from pypdf import PdfReader

from app.core.config import get_settings


def _ollama_client(settings=None) -> tuple[OpenAI, str]:
    settings = settings or get_settings()
    timeout = httpx.Timeout(90.0, connect=10.0)
    client = OpenAI(
        base_url=settings.ollama_base_url,
        api_key="ollama",
        timeout=90.0,
        http_client=httpx.Client(timeout=timeout),
    )
    return client, settings.ollama_model


def get_llm_client() -> tuple[OpenAI, str]:
    settings = get_settings()
    provider = (settings.llm_provider or "ollama").lower()
    if provider in {"ollama", "lab", "auto"}:
        return _ollama_client(settings)
    if provider == "fcc":
        client = OpenAI(
            api_key=settings.fcc_api_key or "fcc",
            base_url=settings.fcc_base_url,
        )
        return client, settings.fcc_model
    client = OpenAI(
        api_key=settings.openai_api_key or "sk-missing",
        base_url=settings.openai_base_url,
    )
    return client, settings.openai_model


def extract_text_from_pdf(path: Path) -> str:
    reader = PdfReader(str(path))
    parts: list[str] = []
    for page in reader.pages:
        parts.append(page.extract_text() or "")
    return "\n".join(parts).strip()


def parse_cv_to_structure(text: str) -> dict[str, Any]:
    lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
    email = None
    phone = None
    for ln in lines[:30]:
        if not email:
            m = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", ln)
            if m:
                email = m.group(0)
        if not phone:
            m = re.search(r"(\+?\d[\d\s().-]{7,}\d)", ln)
            if m:
                phone = m.group(1)
    return {
        "email": email,
        "phone": phone,
        "raw_lines": lines[:200],
        "summary_preview": " ".join(lines[:8])[:500],
    }


IMPROVE_SYSTEM = """Eres un coach senior de carrera IT (programación + sistemas/DevOps).
Mejora el CV en Markdown para ATS y reclutadores internacionales, SIN inventar experiencia.
Reglas estrictas:
- NO inventes empresas, fechas, títulos, certificaciones ni métricas.
- Si falta un dato, usa [PENDIENTE: ...]
- Mantén honestidad sobre nivel de inglés (si no es fluido, no lo maquilles).
- Prioriza impacto: stack, entornos, responsabilidades, resultados medibles solo si ya están en el texto.
- Estructura: Perfil, Skills, Experiencia, Proyectos, Formación, Idiomas.
- Responde SOLO en Markdown del CV."""

ADAPT_SYSTEM = """Eres un experto ATS + career coach IT. Adapta el CV a UNA oferta concreta.
Reglas:
- NO inventes experiencia. Solo reordena, enfatiza y reformula lo existente.
- Extrae keywords de la oferta y alinea skills/bullets (sección final: ## Keywords alineadas).
- Si el candidato NO tiene inglés fluido y la oferta lo exige, incluye ## Nota honesta al final.
- Optimiza para teletrabajo desde España / remoto EU-US cuando la oferta lo permita.
- Devuelve el CV en Markdown. Si se pide carta, después del CV escribe exactamente:
---COVER---
y luego la carta de presentación (idioma pedido), breve y concreta."""


def _chat_openai_compat(system: str, user: str) -> str:
    settings = get_settings()
    client, model = get_llm_client()
    resp = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
        temperature=0.35,
    )
    return (resp.choices[0].message.content or "").strip()


def _chat_cursor_bridge(system: str, user: str) -> str | None:
    """Usa cursor-mobile-bridge (agente Cursor) para CV de alta calidad. Puede tardar."""
    settings = get_settings()
    if not settings.cursor_bridge_url or not settings.cursor_bridge_token:
        return None
    if not settings.cursor_api_enabled:
        return None

    prompt = (
        "MODO: generación de CV (respuesta SOLO Markdown, sin explicación previa).\n\n"
        f"SYSTEM:\n{system}\n\nUSER:\n{user}"
    )
    headers = {
        "Authorization": f"Bearer {settings.cursor_bridge_token}",
        "Content-Type": "application/json",
    }
    base = settings.cursor_bridge_url.rstrip("/")
    payload = {
        "project": settings.cursor_bridge_project,
        "message": prompt,
        "newChat": True,
        "mobileLight": True,
    }
    try:
        with httpx.Client(timeout=30.0) as client:
            start = client.post(f"{base}/api/chat", headers=headers, json=payload)
            if start.status_code == 401:
                return None
            start.raise_for_status()
            data = start.json()
            stream_id = data.get("streamId") or data.get("stream_id")
            if not stream_id:
                return None

        # Poll job result
        deadline = time.time() + max(60, settings.cursor_bridge_timeout_sec)
        chunks: list[str] = []
        with httpx.Client(timeout=60.0) as client:
            while time.time() < deadline:
                job = client.get(f"{base}/api/chat/jobs/{stream_id}", headers=headers)
                if job.status_code >= 400:
                    time.sleep(2)
                    continue
                body = job.json()
                status = (body.get("status") or body.get("state") or "").lower()
                # Collect text from common shapes
                for key in ("result", "finalText", "assistantText", "output", "message"):
                    val = body.get(key)
                    if isinstance(val, str) and val.strip():
                        chunks.append(val.strip())
                events = body.get("events") or body.get("messages") or []
                if isinstance(events, list):
                    for ev in events:
                        if not isinstance(ev, dict):
                            continue
                        t = ev.get("text") or ev.get("content") or ev.get("message")
                        if isinstance(t, str) and t.strip():
                            chunks.append(t.strip())
                if status in {"done", "completed", "success", "finished", "ok"}:
                    break
                if status in {"error", "failed"}:
                    return None
                time.sleep(2)
        if not chunks:
            return None
        # Prefer last substantial markdown-looking chunk
        chunks = sorted(set(chunks), key=len)
        return chunks[-1]
    except Exception:  # noqa: BLE001
        return None


def _chat(system: str, user: str) -> str:
    settings = get_settings()
    provider = (settings.llm_provider or "ollama").lower()

    # Lab Ollama primero (rápido). Cursor solo si provider=cursor, o fallback si Ollama falla en auto.
    def _via_ollama() -> str:
        client, model = _ollama_client(settings)
        resp = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            temperature=0.35,
        )
        return (resp.choices[0].message.content or "").strip()

    if provider in {"ollama", "lab"}:
        try:
            return _via_ollama()
        except Exception as exc:  # noqa: BLE001
            return (
                "### Error IA\n\n"
                f"{exc}\n\n"
                "Comprueba Ollama en 192.168.1.16:11434.\n\n"
                f"---\n\n{user[:1500]}"
            )

    if provider == "cursor" and settings.cursor_api_enabled:
        cursor_out = _chat_cursor_bridge(system, user)
        if cursor_out:
            return cursor_out
        try:
            return _via_ollama()
        except Exception as exc:  # noqa: BLE001
            return (
                "### Error IA\n\n"
                f"{exc}\n\n"
                "Cursor bridge y Ollama fallaron. Revisa :8095 y 192.168.1.16:11434.\n\n"
                f"---\n\n{user[:1500]}"
            )

    if provider == "auto":
        try:
            return _via_ollama()
        except Exception:
            if settings.cursor_api_enabled:
                cursor_out = _chat_cursor_bridge(system, user)
                if cursor_out:
                    return cursor_out
            return (
                "### Error IA\n\n"
                "Ollama lab y Cursor bridge no respondieron.\n\n"
                f"---\n\n{user[:1500]}"
            )

    try:
        return _chat_openai_compat(system, user)
    except Exception as exc:  # noqa: BLE001
        return (
            "### Error IA\n\n"
            f"{exc}\n\n"
            "Comprueba Ollama en 192.168.1.16:11434 o Cursor bridge :8095.\n\n"
            f"---\n\n{user[:1500]}"
        )


def improve_cv(content_md: str, language: str = "es", instructions: str = "") -> str:
    user = (
        f"Idioma de salida: {language}\n"
        f"Contexto candidato: IT amplio (desarrollo + sistemas), teletrabajo desde España, "
        f"interesado en sueldos altos (mercado US/EU), inglés no fluido.\n"
        f"Instrucciones extra: {instructions or 'ninguna'}\n\n"
        f"CV actual:\n{content_md}"
    )
    return _chat(IMPROVE_SYSTEM, user)


def adapt_cv_to_job(
    content_md: str,
    job_title: str,
    company: str,
    description: str,
    language: str = "es",
    include_cover_letter: bool = True,
) -> tuple[str, str, str]:
    user = (
        f"Idioma: {language}\n"
        f"Incluir carta: {'sí' if include_cover_letter else 'no'}\n"
        f"Oferta: {job_title} @ {company}\n"
        f"Descripción:\n{description[:7000]}\n\n"
        f"CV base (fuente de verdad; no inventar):\n{content_md}"
    )
    raw = _chat(ADAPT_SYSTEM, user)
    cover = ""
    cv = raw
    notes = ""
    if "---COVER---" in raw:
        cv, cover = raw.split("---COVER---", 1)
        cv, cover = cv.strip(), cover.strip()
    if "[PENDIENTE]" in raw:
        notes = "Hay huecos marcados como [PENDIENTE]; revísalos antes de enviar."
    # Strip accidental prose before first heading
    if not cv.lstrip().startswith("#"):
        idx = cv.find("\n#")
        if idx > 0:
            cv = cv[idx + 1 :].lstrip()
    return cv, cover, notes


def llm_status() -> dict[str, Any]:
    settings = get_settings()
    out: dict[str, Any] = {
        "provider": settings.llm_provider,
        "ollama_base_url": settings.ollama_base_url,
        "ollama_model": settings.ollama_model,
        "cursor_enabled": settings.cursor_api_enabled,
        "cursor_bridge_url": settings.cursor_bridge_url,
        "ollama_ok": False,
        "models": [],
    }
    try:
        with httpx.Client(timeout=5.0) as client:
            r = client.get(f"{settings.ollama_base_url.replace('/v1', '')}/api/tags")
            if r.status_code == 200:
                out["ollama_ok"] = True
                out["models"] = [m.get("name") for m in r.json().get("models", [])][:12]
    except Exception as exc:  # noqa: BLE001
        out["ollama_error"] = str(exc)
    return out
