"""Confluence + repository retrieval for Requirement Analyst context."""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import List, Optional, Protocol

import httpx

from packages.config import Settings, get_settings
from packages.integrations.sanitize import sanitize_text


@dataclass
class KnowledgeHit:
    source: str
    title: str
    snippet: str
    url: Optional[str] = None


class ConfluenceRetriever(Protocol):
    async def search(self, query: str, limit: int = 5) -> List[KnowledgeHit]: ...


class RepoRetriever(Protocol):
    async def search(self, query: str, limit: int = 5) -> List[KnowledgeHit]: ...


class MockConfluenceRetriever:
    def __init__(self) -> None:
        self.pages = [
            KnowledgeHit(
                source="confluence",
                title="Salesforce Naming Conventions",
                snippet="Custom fields use PascalCase with __c suffix. Apex classes use Service/Handler suffixes.",
                url="https://example.atlassian.net/wiki/spaces/ENG/pages/1",
            ),
            KnowledgeHit(
                source="confluence",
                title="Architecture Standards",
                snippet="Prefer record-triggered Flows for simple automation; use Apex for complex bulk logic.",
                url="https://example.atlassian.net/wiki/spaces/ARCH/pages/2",
            ),
        ]

    def seed(self, hit: KnowledgeHit) -> None:
        self.pages.insert(0, hit)

    async def search(self, query: str, limit: int = 5) -> List[KnowledgeHit]:
        q = query.lower()
        hits = [p for p in self.pages if any(t in p.title.lower() or t in p.snippet.lower() for t in q.split()[:5])]
        if not hits:
            hits = list(self.pages[:limit])
        return [
            KnowledgeHit(
                source=h.source,
                title=h.title,
                snippet=sanitize_text(h.snippet),
                url=h.url,
            )
            for h in hits[:limit]
        ]


class HttpConfluenceRetriever:
    """Live Confluence search using Confluence Cloud content search."""

    def __init__(
        self,
        settings: Optional[Settings] = None,
        transport: Optional[httpx.AsyncBaseTransport] = None,
    ) -> None:
        self.settings = settings or get_settings()
        self.transport = transport
        self._auth = (
            self.settings.confluence_email or self.settings.jira_email,
            self.settings.confluence_api_token or self.settings.jira_api_token,
        )

    async def search(self, query: str, limit: int = 5) -> List[KnowledgeHit]:
        cql = _build_confluence_cql(query, self.settings.confluence_space_key_list)
        params = {
            "cql": cql,
            "limit": max(1, min(limit, 25)),
            "expand": "body.storage",
        }
        async with httpx.AsyncClient(
            timeout=30.0,
            auth=self._auth,
            transport=self.transport,
        ) as client:
            resp = await client.get(self._url("/rest/api/content/search"), params=params)
            resp.raise_for_status()
            data = resp.json()
        return _parse_confluence_search_results(data, self.settings.confluence_base_url, limit=limit)

    def _url(self, path: str) -> str:
        return f"{self.settings.confluence_base_url.rstrip('/')}{path}"


class MockRepoRetriever:
    def __init__(self) -> None:
        self.files = [
            KnowledgeHit(
                source="git",
                title="force-app/main/default/classes/AccountService.cls",
                snippet="public with sharing class AccountService { /* pattern for Account domain logic */ }",
            ),
            KnowledgeHit(
                source="git",
                title="force-app/main/default/objects/Account/fields/Industry__c.field-meta.xml",
                snippet="Custom field Industry__c on Account — example field metadata pattern.",
            ),
            KnowledgeHit(
                source="git",
                title="force-app/main/default/lwc/accountSummary/accountSummary.js",
                snippet="Lightning Web Component pattern for Account summary display.",
            ),
        ]

    async def search(self, query: str, limit: int = 5) -> List[KnowledgeHit]:
        q = query.lower()
        hits = [f for f in self.files if any(t in f.title.lower() or t in f.snippet.lower() for t in q.split()[:5])]
        if not hits:
            hits = self.files[:limit]
        return hits[:limit]


class KnowledgeService:
    def __init__(
        self,
        confluence: Optional[ConfluenceRetriever] = None,
        repo: Optional[RepoRetriever] = None,
        settings: Optional[Settings] = None,
    ) -> None:
        self.settings = settings or get_settings()
        self.confluence = confluence or get_confluence_retriever(self.settings)
        self.repo = repo or get_repo_retriever(self.settings)

    async def gather(self, query: str, limit: int = 5) -> List[KnowledgeHit]:
        conf = await self.confluence.search(query, limit=limit)
        repo = await self.repo.search(query, limit=limit)
        return conf + repo


def get_confluence_retriever(settings: Optional[Settings] = None) -> ConfluenceRetriever:
    settings = settings or get_settings()
    if settings.confluence_provider == "http":
        return HttpConfluenceRetriever(settings)
    return MockConfluenceRetriever()


def get_repo_retriever(settings: Optional[Settings] = None) -> RepoRetriever:
    settings = settings or get_settings()
    return MockRepoRetriever()


_CQL_STOPWORDS = frozenset(
    {
        "the",
        "and",
        "for",
        "with",
        "that",
        "this",
        "from",
        "into",
        "onto",
        "have",
        "has",
        "will",
        "should",
        "please",
        "need",
        "needs",
        "add",
        "added",
        "create",
        "created",
        "update",
        "using",
        "when",
        "each",
        "also",
        "just",
        "test",
        "testing",
        "agent",
        "nova",
        "point",
        "story",
        "ticket",
        "intentionally",
        "duplicate",
        "duplicates",
        "semantic",
        "check",
        "retest",
    }
)


def _confluence_search_terms(query: str, *, max_terms: int = 6) -> List[str]:
    """Extract short significant terms for Confluence CQL (full phrases often miss)."""
    raw = re.sub(r"\s+", " ", (query or "").strip())
    if not raw:
        return []
    tokens = re.findall(r"[A-Za-z][A-Za-z0-9_]{2,}", raw)
    field_terms: List[str] = []
    other_terms: List[str] = []
    seen: set[str] = set()
    for token in tokens:
        low = token.lower()
        if low in _CQL_STOPWORDS or low in seen:
            continue
        # Point5 / AN-58 style run labels are noise for doc search
        if re.fullmatch(r"point\d+[a-z]?", low) or re.fullmatch(r"an[-_]?\d+", low):
            continue
        seen.add(low)
        if "__c" in low:
            field_terms.append(token)
        else:
            other_terms.append(token)
    # Prefer readable title words first; API names alone are weak in title~
    return (other_terms + field_terms)[:max_terms]


def _build_confluence_cql(query: str, spaces: List[str]) -> str:
    space_clause = ""
    if spaces:
        quoted = ", ".join(f'"{space}"' for space in spaces)
        space_clause = f"space in ({quoted}) AND "

    raw = re.sub(r"\s+", " ", (query or "").strip())
    # Short queries keep a single phrase (existing behavior / unit tests).
    if raw and len(raw) <= 40:
        safe = raw.replace('"', '\\"')
        return f'{space_clause}type = page AND (title ~ "{safe}" OR text ~ "{safe}")'

    terms = _confluence_search_terms(query)
    if not terms:
        safe = raw[:40].replace('"', '\\"')
        if not safe:
            return f"{space_clause}type = page"
        return f'{space_clause}type = page AND (title ~ "{safe}" OR text ~ "{safe}")'

    parts = [f'title ~ "{t.replace(chr(34), "")}" OR text ~ "{t.replace(chr(34), "")}"' for t in terms]
    return f"{space_clause}type = page AND ({' OR '.join(parts)})"


def _parse_confluence_search_results(
    payload: dict,
    base_url: str,
    *,
    limit: int = 5,
) -> List[KnowledgeHit]:
    results = payload.get("results") or []
    hits: List[KnowledgeHit] = []
    for result in results[:limit]:
        content = result.get("content") or result
        page_id = str(content.get("id") or result.get("id") or "")
        title = sanitize_text(str(content.get("title") or result.get("title") or "Untitled"))
        storage = (
            (((content.get("body") or {}).get("storage") or {}).get("value"))
            or (((result.get("body") or {}).get("storage") or {}).get("value"))
            or ""
        )
        excerpt = sanitize_text(_html_to_text(storage or result.get("excerpt") or ""), max_len=400)
        url = _page_url(base_url, content if content.get("_links") else result, page_id)
        hits.append(
            KnowledgeHit(
                source="confluence",
                title=title,
                snippet=excerpt or "(no excerpt available)",
                url=url,
            )
        )
    return hits


def _page_url(base_url: str, content: dict, page_id: str) -> Optional[str]:
    links = content.get("_links") or {}
    webui = links.get("webui")
    if webui:
        if webui.startswith("http://") or webui.startswith("https://"):
            return webui
        return f"{base_url.rstrip('/')}{webui}"
    if page_id:
        return f"{base_url.rstrip('/')}/spaces/pages/{page_id}"
    return None


def _html_to_text(value: str) -> str:
    text = re.sub(r"<[^>]+>", " ", value or "")
    text = text.replace("&nbsp;", " ")
    text = text.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
    text = re.sub(r"\s+", " ", text)
    return text.strip()
