from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, List, Dict
import uuid, re, math, asyncio, httpx
from datetime import datetime
from html.parser import HTMLParser
from collections import Counter

app = FastAPI(title="LiveChat AI Agent API", version="2.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
                   allow_methods=["*"], allow_headers=["*"])

# ── In-memory stores ──────────────────────────────────────────────────────────
sessions:         Dict[str, dict] = {}
website_configs:  Dict[str, dict] = {}
knowledge_chunks: Dict[str, List[dict]] = {}   # config_id → list of {text, tokens, tfidf}

# ── Stopwords (lightweight) ───────────────────────────────────────────────────
STOPWORDS = {
    "a","an","the","and","or","but","in","on","at","to","for","of","with",
    "is","are","was","were","be","been","being","have","has","had","do","does",
    "did","will","would","could","should","may","might","shall","can","it","its",
    "this","that","these","those","i","me","my","we","our","you","your","he","she",
    "they","them","their","what","which","who","how","when","where","why","not",
    "no","so","if","as","by","from","up","about","into","through","during","more",
}

# ── HTML extractor ────────────────────────────────────────────────────────────
class HTMLTextExtractor(HTMLParser):
    SKIP = {"script","style","head","noscript","svg","iframe","nav","footer"}
    def __init__(self):
        super().__init__(); self.parts: List[str] = []; self._skip = 0
    def handle_starttag(self, tag, attrs):
        if tag.lower() in self.SKIP: self._skip += 1
    def handle_endtag(self, tag):
        if tag.lower() in self.SKIP: self._skip = max(0, self._skip - 1)
    def handle_data(self, data):
        if self._skip == 0:
            s = data.strip()
            if s: self.parts.append(s)
    def text(self) -> str:
        return " ".join(self.parts)

def html_to_text(html: str) -> str:
    p = HTMLTextExtractor(); p.feed(html)
    return re.sub(r"\s{3,}", "  ", p.text())

async def fetch_text(url: str) -> str:
    try:
        async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c:
            r = await c.get(url, headers={"User-Agent": "Mozilla/5.0 (LiveChatBot/2.0)"})
            r.raise_for_status()
            return html_to_text(r.text)
    except Exception as e:
        return ""

# ── NLP helpers ───────────────────────────────────────────────────────────────
def tokenize(text: str) -> List[str]:
    return [w.lower() for w in re.findall(r"[a-zA-Z']{2,}", text) if w.lower() not in STOPWORDS]

def chunk_text(text: str, size: int = 120, overlap: int = 30) -> List[str]:
    words = text.split()
    chunks = []
    i = 0
    while i < len(words):
        chunks.append(" ".join(words[i:i+size]))
        i += size - overlap
    return [c for c in chunks if len(c.strip()) > 40]

def build_tfidf(all_chunks: List[str]) -> List[dict]:
    """Build TF-IDF vectors for all chunks."""
    tokenized = [tokenize(c) for c in all_chunks]
    N = len(tokenized)
    # DF counts
    df: Counter = Counter()
    for tokens in tokenized:
        df.update(set(tokens))
    # IDF
    idf = {w: math.log((N + 1) / (df[w] + 1)) + 1 for w in df}

    result = []
    for text, tokens in zip(all_chunks, tokenized):
        tf = Counter(tokens)
        total = max(len(tokens), 1)
        vec = {w: (tf[w] / total) * idf.get(w, 1) for w in tf}
        norm = math.sqrt(sum(v*v for v in vec.values())) or 1
        result.append({"text": text, "vec": vec, "norm": norm})
    return result

def cosine(q_vec: dict, q_norm: float, chunk: dict) -> float:
    dot = sum(q_vec.get(w, 0) * v for w, v in chunk["vec"].items())
    return dot / (q_norm * chunk["norm"])

def retrieve(query: str, chunks: List[dict], top_k: int = 5) -> List[str]:
    tokens = tokenize(query)
    if not tokens or not chunks:
        return []
    tf = Counter(tokens)
    total = len(tokens)
    q_vec = {w: tf[w] / total for w in tf}
    q_norm = math.sqrt(sum(v*v for v in q_vec.values())) or 1
    scored = [(cosine(q_vec, q_norm, c), c["text"]) for c in chunks]
    scored.sort(key=lambda x: -x[0])
    return [text for score, text in scored[:top_k] if score > 0.05]

# ── Intent detection ──────────────────────────────────────────────────────────
INTENTS = {
    "greeting":    r"\b(hi|hello|hey|good\s*(morning|afternoon|evening)|howdy|sup)\b",
    "farewell":    r"\b(bye|goodbye|see\s*you|thanks?\s*(so\s*much)?|thank\s*you|cheers|ciao)\b",
    "transfer":    r"\b(human|real\s*person|live\s*agent|customer\s*care|speak\s*to\s*someone|talk\s*to\s*someone|representative|supervisor|manager|support\s*team)\b",
    "pricing":     r"\b(pric(e|ing|es)|cost|fee|cheap|expensive|plan|subscription|how\s*much|afford)\b",
    "contact":     r"\b(contact|email|phone|call|reach|address|location|office|headquarters)\b",
    "hours":       r"\b(hour|open|close|available|time|schedule|when\s*do\s*you)\b",
    "about":       r"\b(what\s*(is|are|do)\s*(you|your)|about\s*you|who\s*are\s*you|tell\s*me\s*about|explain|describe|overview)\b",
    "product":     r"\b(product|service|offer|feature|solution|provide|deliver|work|use|function)\b",
    "help":        r"\b(help|assist|support|problem|issue|trouble|error|fix|solve|cant|cannot|doesn'?t)\b",
}

def detect_intent(text: str) -> str:
    t = text.lower()
    for intent, pattern in INTENTS.items():
        if re.search(pattern, t):
            return intent
    return "general"

# ── Response generator ─────────────────────────────────────────────────────────
GREETING_REPLIES = [
    "Hi there! Great to hear from you. What can I help you with today?",
    "Hello! Happy to help — what's on your mind?",
    "Hey! Welcome. How can I assist you today?",
]
FAREWELL_REPLIES = [
    "You're welcome! Have a wonderful day! 👋",
    "Glad I could help. Take care!",
    "Thanks for chatting — feel free to come back anytime!",
]

import random

def build_answer(intent: str, context_chunks: List[str], query: str, cfg: dict) -> tuple[str, str]:
    """Returns (reply_text, confidence: 'high'|'low')"""
    name = cfg.get("business_name", "us")
    agent = cfg.get("agent_name", "I")

    if intent == "greeting":
        return random.choice(GREETING_REPLIES), "high"

    if intent == "farewell":
        return random.choice(FAREWELL_REPLIES), "high"

    if intent == "transfer":
        return (
            "Sure! I can connect you with a member of our support team. "
            "Click **'👤 Human'** in the top-right of this chat to be transferred.",
            "high"
        )

    if not context_chunks:
        return (
            f"I don't have specific information about that on hand. "
            f"For the most accurate answer, I'd recommend checking the {name} website directly "
            f"or speaking with one of our support agents.",
            "low"
        )

    # Stitch top chunks into a coherent answer
    combined = " ".join(context_chunks[:3])

    # Extract the most relevant sentences
    sentences = re.split(r'(?<=[.!?])\s+', combined)
    query_tokens = set(tokenize(query))

    scored_sentences = []
    for s in sentences:
        if len(s) < 20: continue
        s_tokens = set(tokenize(s))
        overlap = len(query_tokens & s_tokens)
        scored_sentences.append((overlap, s))

    scored_sentences.sort(key=lambda x: -x[0])
    top = [s for _, s in scored_sentences[:4]]

    if not top:
        return (
            f"Based on what I know about {name}: {combined[:300]}...",
            "low"
        )

    answer = " ".join(top)
    # Trim to reasonable length
    if len(answer) > 500:
        answer = answer[:497] + "..."

    # Prefix with a helpful lead-in
    leads = [
        f"Based on {name}'s information: ",
        f"Here's what I found: ",
        f"Great question! ",
        f"",
    ]
    answer = random.choice(leads) + answer

    # Confidence: high if we had solid matches
    confidence = "high" if scored_sentences and scored_sentences[0][0] >= 2 else "low"
    return answer, confidence


async def build_knowledge(config_id: str, config: dict):
    base = config["website_url"].rstrip("/")
    urls = [base, f"{base}/about", f"{base}/about-us"]
    if config.get("extra_pages"):
        urls += [u.rstrip("/") for u in config["extra_pages"]]

    pages = await asyncio.gather(*[fetch_text(u) for u in urls])

    all_text = "\n\n".join(
        f"[Page: {u}]\n{t}" for u, t in zip(urls, pages) if t.strip()
    )

    if not all_text.strip():
        knowledge_chunks[config_id] = []
        return

    raw_chunks = chunk_text(all_text)
    knowledge_chunks[config_id] = build_tfidf(raw_chunks)


# ── Pydantic models ───────────────────────────────────────────────────────────
class WebsiteConfig(BaseModel):
    website_url: str
    business_name: str
    agent_name: str = "Alex"
    primary_color: str = "#6366f1"
    welcome_message: str = "Hi! How can I help you today?"
    transfer_threshold: int = 3
    extra_pages: Optional[List[str]] = []

class ChatMessage(BaseModel):
    session_id: str
    message: str
    config_id: str

class TransferRequest(BaseModel):
    session_id: str
    config_id: str
    reason: Optional[str] = "User requested human agent"

class StartSession(BaseModel):
    config_id: str
    visitor_name: Optional[str] = "Visitor"


# ── Routes ────────────────────────────────────────────────────────────────────
@app.post("/api/config")
async def create_config(config: WebsiteConfig, bg: BackgroundTasks):
    config_id = str(uuid.uuid4())
    data = config.dict()
    data["created_at"] = datetime.utcnow().isoformat()
    website_configs[config_id] = data
    bg.add_task(build_knowledge, config_id, data)
    return {"config_id": config_id, "message": "Config saved. Knowledge base building in background."}

@app.get("/api/config/{config_id}")
async def get_config(config_id: str):
    cfg = website_configs.get(config_id)
    if not cfg: raise HTTPException(404, "Config not found")
    return cfg

@app.post("/api/config/{config_id}/refresh")
async def refresh(config_id: str, bg: BackgroundTasks):
    cfg = website_configs.get(config_id)
    if not cfg: raise HTTPException(404, "Config not found")
    bg.add_task(build_knowledge, config_id, cfg)
    return {"message": "Knowledge base refresh started"}

@app.post("/api/session/start")
async def start_session(body: StartSession):
    if body.config_id not in website_configs:
        raise HTTPException(404, "Config not found")
    cfg = website_configs[body.config_id]
    sid = str(uuid.uuid4())
    sessions[sid] = {
        "config_id": body.config_id,
        "visitor_name": body.visitor_name,
        "history": [],
        "low_confidence_count": 0,
        "transferred": False,
        "transfer_requested": False,
        "created_at": datetime.utcnow().isoformat(),
    }
    return {
        "session_id": sid,
        "welcome_message": cfg["welcome_message"],
        "agent_name": cfg["agent_name"],
        "business_name": cfg["business_name"],
    }

@app.post("/api/chat")
async def chat(body: ChatMessage):
    session = sessions.get(body.session_id)
    if not session: raise HTTPException(404, "Session not found")

    if session.get("transferred"):
        return {"reply": "You're connected to a human agent. They'll be with you shortly.",
                "transferred": True, "suggested_transfer": False}

    cfg = website_configs.get(body.config_id)
    if not cfg: raise HTTPException(404, "Config not found")

    msg = body.message.strip()
    intent = detect_intent(msg)

    # Auto-flag transfer intent
    if intent == "transfer":
        session["transfer_requested"] = True

    # Retrieve relevant chunks
    chunks = knowledge_chunks.get(body.config_id, [])
    context = retrieve(msg, chunks, top_k=5)

    # Generate reply
    reply_text, confidence = build_answer(intent, context, msg, cfg)

    # Confidence tracking
    if confidence == "low":
        session["low_confidence_count"] += 1
    else:
        session["low_confidence_count"] = max(0, session["low_confidence_count"] - 1)

    session["history"].append({"user": msg, "assistant": reply_text, "intent": intent})

    suggest_transfer = (
        session["low_confidence_count"] >= cfg.get("transfer_threshold", 3)
        or session.get("transfer_requested")
    )

    if suggest_transfer and not session.get("transfer_suggested_once"):
        session["transfer_suggested_once"] = True
        reply_text += "\n\n💬 Would you like me to connect you with a human support agent who can help further?"

    return {
        "reply": reply_text,
        "confidence": confidence,
        "intent": intent,
        "transferred": False,
        "suggested_transfer": suggest_transfer,
        "session_id": body.session_id,
    }

@app.post("/api/transfer")
async def transfer(body: TransferRequest):
    session = sessions.get(body.session_id)
    if not session: raise HTTPException(404, "Session not found")
    cfg = website_configs.get(body.config_id, {})
    session["transferred"] = True
    session["transfer_reason"] = body.reason
    session["transferred_at"] = datetime.utcnow().isoformat()

    transcript = []
    for t in session["history"]:
        transcript.append(f"Visitor: {t['user']}")
        transcript.append(f"Agent: {t['assistant']}")

    return {
        "message": "You've been connected to a human support agent. They'll review your chat and respond shortly.",
        "ticket_id": f"TKT-{body.session_id[:8].upper()}",
        "transcript_summary": "\n".join(transcript[-10:]),
        "visitor_name": session.get("visitor_name"),
        "business_name": cfg.get("business_name", ""),
    }

@app.get("/api/session/{session_id}")
async def get_session(session_id: str):
    s = sessions.get(session_id)
    if not s: raise HTTPException(404, "Session not found")
    return s

@app.get("/api/sessions")
async def list_sessions():
    return [{"session_id": sid, "config_id": s["config_id"],
             "visitor_name": s.get("visitor_name"), "messages": len(s["history"]),
             "transferred": s.get("transferred", False), "created_at": s.get("created_at")}
            for sid, s in sessions.items()]

@app.get("/health")
async def health():
    return {"status": "ok", "sessions": len(sessions), "configs": len(website_configs),
            "knowledge_bases": {k: len(v) for k, v in knowledge_chunks.items()}}
