"""
scrylo/core/llm_adapter.py
---------------------------
Pluggable LLM chain for Scrylo. Detects what's available and falls back
gracefully. User never sees an error — worst case they get a Jinja2 template.

Priority order:
  1. Groq API        — BYOK, free tier, Llama 3.1 70B, best quality, fast
  2. Ollama          — local power users running their own models
  3. Jinja2 template — always works, --no-llm flag, zero dependencies
"""

from __future__ import annotations

import logging
import re
import textwrap
from dataclasses import dataclass
from typing import Any

from scrylo.core.config import load as load_cfg

log = logging.getLogger("scrylo.llm")

# Groq free tier models
_GROQ_PITCH_MODEL = "llama-3.3-70b-versatile"
_GROQ_PARSE_MODEL = "llama-3.1-8b-instant"
_GROQ_FALLBACK    = "meta-llama/llama-4-scout-17b-16e-instruct"


# ─────────────────────────────────────────────────────────────────────────────
# Result type
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class LLMResult:
    text: str
    provider: str      # "groq" | "ollama" | "jinja2"
    model: str
    tokens_used: int = 0
    error: str = ""

    @property
    def ok(self) -> bool:
        return bool(self.text) and not self.error


# ─────────────────────────────────────────────────────────────────────────────
# Provider 1 — Groq (BYOK)
# ─────────────────────────────────────────────────────────────────────────────

def _groq(system: str, user: str, api_key: str,
          model: str = _GROQ_PITCH_MODEL,
          max_tokens: int = 400, temperature: float = 0.7) -> LLMResult:
    if not api_key:
        return LLMResult("", "groq", model, error="no api key")
    try:
        import httpx
        resp = httpx.post(
            "https://api.groq.com/openai/v1/chat/completions",
            headers={"Authorization": f"Bearer {api_key}",
                     "Content-Type": "application/json"},
            json={"model": model,
                  "messages": [{"role": "system", "content": system},
                                {"role": "user",   "content": user}],
                  "max_tokens": max_tokens,
                  "temperature": temperature},
            timeout=20,
        )
        if resp.status_code == 404 and model != _GROQ_FALLBACK:
            return _groq(system, user, api_key, _GROQ_FALLBACK,
                         max_tokens, temperature)
        if resp.status_code == 429:
            return LLMResult("", "groq", model, error="quota exceeded")
        if resp.status_code == 401:
            return LLMResult("", "groq", model, error="invalid api key")
        if resp.status_code != 200:
            return LLMResult("", "groq", model, error=f"HTTP {resp.status_code}")
        data    = resp.json()
        content = data["choices"][0]["message"]["content"].strip()
        tokens  = data.get("usage", {}).get("total_tokens", 0)
        log.info("Groq %s -> %d tokens", model, tokens)
        return LLMResult(content, "groq", model, tokens_used=tokens)
    except Exception as e:
        log.warning("Groq error: %s", e)
        return LLMResult("", "groq", model, error=str(e))


def verify_groq_key(api_key: str) -> tuple[bool, str]:
    """Check key validity + return best available model name."""
    if not api_key:
        return False, ""
    try:
        import httpx
        resp = httpx.get(
            "https://api.groq.com/openai/v1/models",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=8,
        )
        if resp.status_code == 200:
            models = [m["id"] for m in resp.json().get("data", [])]
            preferred = next(
                (m for m in [_GROQ_PITCH_MODEL, _GROQ_PARSE_MODEL, _GROQ_FALLBACK]
                 if m in models),
                models[0] if models else _GROQ_FALLBACK,
            )
            return True, preferred
        return False, ""
    except Exception:
        return False, ""


# ─────────────────────────────────────────────────────────────────────────────
# Provider 2 — Ollama
# ─────────────────────────────────────────────────────────────────────────────

def _ollama_available() -> bool:
    """Check if Ollama is reachable."""
    try:
        import httpx
        r = httpx.get("http://localhost:11434/api/tags", timeout=2)
        return r.status_code == 200
    except Exception:
        return False


def _list_ollama_models() -> list[str]:
    """Get list of all model names pulled in Ollama."""
    try:
        import httpx
        r = httpx.get("http://localhost:11434/api/tags", timeout=2)
        if r.status_code == 200:
            return [m["name"] for m in r.json().get("models", [])]
    except Exception:
        pass
    return []


def _best_ollama_model() -> str:
    """Select the best available model from Ollama, or llama3 as default."""
    models = _list_ollama_models()
    if not models:
        return "llama3"
    
    # Priority order for models we know work well with Scrylo's prompts
    for pref in ["llama3.3", "llama3.1:70b", "llama3.1", "llama3.2", "llama3",
                 "mistral", "mixtral", "gemma2"]:
        match = next((m for m in models if pref in m), None)
        if match:
            return match
    return models[0]


def _ollama(system: str, user: str, model: str = "llama3",
            max_tokens: int = 400, temperature: float = 0.7) -> LLMResult:
    try:
        import httpx
        resp = httpx.post(
            "http://localhost:11434/api/chat",
            json={"model": model,
                  "messages": [{"role": "system", "content": system},
                                {"role": "user",   "content": user}],
                  "options": {"temperature": temperature,
                              "num_predict": max_tokens},
                  "stream": False},
            timeout=60,
        )
        if resp.status_code == 200:
            content = resp.json()["message"]["content"].strip()
            return LLMResult(content, "ollama", model)
        return LLMResult("", "ollama", model, error=f"HTTP {resp.status_code}")
    except Exception as e:
        return LLMResult("", "ollama", model, error=str(e))


# ─────────────────────────────────────────────────────────────────────────────
# Provider 3 — Jinja2 template (always works)
# ─────────────────────────────────────────────────────────────────────────────

_SUBJECT_TEMPLATES = [
    "Scaling outbound at {company}",
    "Quick question, {first_name}",
    "Idea for {company}",
    "{company} + [our service]",
    "15 mins — {first_name}?",
]

_HOOKS_WITH_SERVICE = [
    "I came across {company} while looking into {industry} companies in {location} and wanted to reach out directly.",
    "I've been following {company}'s growth in the {industry} space — impressive work.",
    "Noticed {company} has been expanding fast — congrats on the momentum.",
]

_HOOKS_NO_SERVICE = [
    "I came across {company} while researching {industry} founders and wanted to reach out.",
    "Your work as {role} at {company} caught my attention.",
    "I've been looking into {industry} companies in {location} and {company} stood out.",
]

_VALUE_WITH_SERVICE = [
    "{service_description} — something that might be relevant given where {company} is headed.",
    "We work with {industry} founders like yourself: {service_description}",
    "Specifically, {service_description} — thought it might be worth a conversation.",
]

_VALUE_NO_SERVICE = [
    "I work with {industry} founders on scaling their outbound pipeline without burning the team.",
    "Most {role}s I speak with are focused on growth without adding headcount — curious if that resonates.",
    "I had a thought about how {company} could tighten up outbound — happy to share if useful.",
]


def _jinja2_pitch(lead_data: dict, service_description: str = "") -> LLMResult:
    import random
    fn   = (lead_data.get("full_name") or "there").split()[0]
    co   = lead_data.get("company") or ""
    role = lead_data.get("role") or "Founder"
    loc  = lead_data.get("location") or "your market"
    ind  = lead_data.get("industry") or "SaaS"
    cm   = lead_data.get("contact_method") or "email"
    sndr = (load_cfg().get("email", {}).get("from_name") or "").strip() or "The Scrylo Team"
    svc  = service_description.strip()

    ctx = dict(company=co or "your company", industry=ind, location=loc, role=role,
               first_name=fn, service_description=svc)

    if svc:
        hook = random.choice(_HOOKS_WITH_SERVICE).format(**ctx)
        vp   = random.choice(_VALUE_WITH_SERVICE).format(**ctx)
    else:
        hook = random.choice(_HOOKS_NO_SERVICE).format(**ctx)
        vp   = random.choice(_VALUE_NO_SERVICE).format(**ctx)

    if cm == "social":
        # Shorter social template
        text = f"Hi {fn},\n\n{hook} {vp}\n\nWorth a quick chat?"
    else:
        body = (
            f"Hi {fn},\n\n"
            f"{hook}\n\n"
            f"{vp}\n\n"
            f"Worth a quick chat this week?\n\n"
            f"{sndr}"
        )
        subject = random.choice(_SUBJECT_TEMPLATES).format(**ctx)
        text = f"{body}\nSUBJECT: {subject}"
    
    return LLMResult(text.strip(), "jinja2", "template")


# ─────────────────────────────────────────────────────────────────────────────
# Internal routing
# ─────────────────────────────────────────────────────────────────────────────

def _provider_order(groq_key: str, force: str = "") -> list[str]:
    """Determine the order of LLM providers to attempt."""
    if force in ("groq", "ollama", "jinja2"):
        return [force]
    
    order = []
    if groq_key:
        order.append("groq")
    
    if _ollama_available() and _list_ollama_models():
        order.append("ollama")
        
    return order


def _dispatch(provider: str, system: str, user: str, groq_key: str,
              max_tokens: int, temperature: float,
              use_parse_model: bool = False) -> LLMResult:
    if provider == "groq":
        m = _GROQ_PARSE_MODEL if use_parse_model else _GROQ_PITCH_MODEL
        return _groq(system, user, groq_key, m, max_tokens, temperature)
    if provider == "ollama":
        return _ollama(system, user, _best_ollama_model(),
                       max_tokens, temperature)
    # No direct dispatch for jinja2 — caller falls back to _jinja2_pitch if no LLM results
    return LLMResult("", provider, "", error="unknown provider")


# ─────────────────────────────────────────────────────────────────────────────
# Specialized System Prompts
# ─────────────────────────────────────────────────────────────────────────────

_EMAIL_WITH_SERVICE_PROMPT = textwrap.dedent("""
    You are a professional B2B email copywriter. Write a specific, short cold email.
    
    STRICT RULES:
    1. SUBJECT: Provide a subject line under 7 words on the last line starting with "SUBJECT: ".
    2. NO HALLUCINATION: If 'company' is missing, do NOT invent one. Pivot the hook to their 'role'.
    3. NO ASSUMPTIONS: Do NOT guess what the lead's company does. Stick to the sender's service.
    4. HOOK: One concrete observation about their role or industry.
    5. PITCH: State exactly what the sender does based on the Service Description.
    6. CTA: One soft question about a 15-min chat.
    7. BANNED: "hope this finds you well", "innovative", "synergy", "cutting-edge", "I'd love to".
    8. Format: Plain text only, under 100 words. Write ONLY the email.
""").strip()

_EMAIL_NO_SERVICE_PROMPT = textwrap.dedent("""
    You are a professional B2B email copywriter. Write a specific, short inquiry.
    
    STRICT RULES:
    1. SUBJECT: Provide a subject line under 7 words on the last line starting with "SUBJECT: ".
    2. NO HALLUCINATION: If 'company' is missing, do NOT invent one. Pivot to their 'role'.
    3. NO ASSUMPTIONS: Do NOT guess what the lead's company does.
    4. HOOK: One concrete observation about their role or industry.
    5. INQUIRY: Ask a genuine, high-signal question about a challenge in their role.
    6. CTA: One soft question about sharing insights.
    7. Format: Plain text only, under 80 words. Write ONLY the email.
""").strip()

_SOCIAL_WITH_SERVICE_PROMPT = textwrap.dedent("""
    You are writing a conversational DM (LinkedIn/GitHub). Tone: Direct, peer-to-peer.
    
    STRICT RULES:
    1. NO SUBJECT LINE. Start directly with "Hi [Name],".
    2. ULTRA-SHORT: Under 60 words. No "fluff" or "filler".
    3. NO HALLUCINATION: If 'company' is missing, do NOT invent one. Mention their 'role'.
    4. NO ASSUMPTIONS: Do NOT guess what the lead's company does.
    5. PITCH: One punchy sentence on how the sender's service helps people in their role.
    6. CTA: Low-friction question (e.g., "Worth a look?", "Open to a chat?").
    7. BANNED: "SUBJECT:", signatures, formal sign-offs, "Dear".
    8. Format: Plain text only. Write ONLY the message.
""").strip()

_SOCIAL_NO_SERVICE_PROMPT = textwrap.dedent("""
    You are writing a conversational DM (LinkedIn/GitHub). Tone: Direct, peer-to-peer.
    
    STRICT RULES:
    1. NO SUBJECT LINE. Start directly with "Hi [Name],".
    2. ULTRA-SHORT: Under 50 words.
    3. NO HALLUCINATION: If 'company' is missing, do NOT invent one.
    4. INQUIRY: Ask one smart question about their work or role.
    5. CTA: Very soft (e.g. "Curious to hear your thoughts.").
    6. BANNED: "SUBJECT:", signatures, pitches.
""").strip()

_PARSE_SYSTEM = textwrap.dedent("""
    Extract all real human people and their roles from the provided text.
    Output ONLY a JSON array of objects with keys: name, role.
    Example: [{"name": "John Doe", "role": "CEO"}]
""").strip()


# ─────────────────────────────────────────────────────────────────────────────
# Public API
# ─────────────────────────────────────────────────────────────────────────────

def generate_pitch(lead_data: dict, force_provider: str = "") -> LLMResult:
    """
    Generate a personalised cold email or social message for one lead.
    """
    cfg      = load_cfg()
    groq_key = (cfg.get("llm", {}).get("groq_api_key") or "").strip()
    cm       = lead_data.get("contact_method", "email")
    svc      = (lead_data.get("service_description") or
                cfg.get("pitch", {}).get("service_description") or "").strip()

    # Route to correct system prompt
    if cm == "social":
        system = _SOCIAL_WITH_SERVICE_PROMPT if svc else _SOCIAL_NO_SERVICE_PROMPT
    else:
        system = _EMAIL_WITH_SERVICE_PROMPT if svc else _EMAIL_NO_SERVICE_PROMPT

    # Build user prompt
    parts = [
        f"Lead: {lead_data.get('full_name','?')}, "
        f"{lead_data.get('role','Founder')}" + (f" at {lead_data['company']}" if lead_data.get('company') else " (Company unknown)")
    ]
    for k, label in [("location","Location"), ("industry","Industry"),
                     ("pain_points","Pain points")]:
        if lead_data.get(k):
            parts.append(f"{label}: {lead_data[k]}")
    
    if svc:
        parts.append(f"\nSender's service: {svc[:500]}")
    
    parts.append(f"\nWrite the {cm} message now:")
    user = "\n".join(parts)

    for provider in _provider_order(groq_key, force_provider):
        if provider == "jinja2":
            return _jinja2_pitch(lead_data, svc)
            
        r = _dispatch(provider, system, user, groq_key,
                      max_tokens=450, temperature=0.75)
        if r.ok:
            log.info("Pitch via %s/%s", r.provider, r.model)
            return r
        log.debug("Provider %s failed: %s", provider, r.error)

    return _jinja2_pitch(lead_data, svc)


def parse_page_for_leads(page_text: str) -> LLMResult:
    """
    Extract structured lead JSON from scraped page text.
    """
    cfg      = load_cfg()
    groq_key = (cfg.get("llm", {}).get("groq_api_key") or "").strip()

    # Truncate to model context limits
    max_chars = 5500 if groq_key else 1400
    text = page_text[:max_chars]
    if len(page_text) > max_chars:
        text += "\n[page truncated]"

    user = f"Extract all people from this page:\n\n{text}"

    for provider in _provider_order(groq_key):
        if provider == "jinja2":
            continue # Can't extract JSON with Jinja2
            
        r = _dispatch(provider, _PARSE_SYSTEM, user, groq_key,
                      max_tokens=800, temperature=0.1,
                      use_parse_model=True)
        if r.ok:
            # Validate JSON
            try:
                import json
                json.loads(r.text)
                return r
            except Exception:
                match = re.search(r"\[.*\]", r.text, re.DOTALL)
                if match:
                    r.text = match.group(0)
                    return r
                continue

    return LLMResult("[]", "jinja2", "template")
