"""
Context Adapter - HTTP-based bridge to CodeContext server

Uses HTTP API instead of direct LanceDB access.
Server must be running at the configured URL.
"""

import re
import sys
import json
import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Dict, Optional

# Import new path structure
from .cli_utils import CODECONTEXT_ROOT
from .utils.timestamps import utc_now_iso

# Queue file for resilient submissions when server is down
# NEW: Use CODECONTEXT_ROOT structure
QUEUE_FILE = CODECONTEXT_ROOT / "pending_queue.jsonl"
QUEUE_LOCK = CODECONTEXT_ROOT / "pending_queue.lock"


class ContextAdapter:
    """
    HTTP-based adapter for CodeContext.

    Usage:
        adapter = ContextAdapter(project="my-project")

        # Store triage results
        adapter.store_from_xml(xml_string)

        # Get context for Claude
        context = adapter.get_context("Add authentication")
    """

    VALID_TYPES = {"bugfix", "feature", "discovery", "decision", "refactor", "optimization", "gotcha", "resolution"}

    def __init__(self, project: str = "default", server_url: str = "http://localhost:8765"):
        """
        Initialize adapter.

        Args:
            project: Project name for memory isolation
            server_url: CodeContext server URL
        """
        self.project = project
        self.server_url = server_url.rstrip("/")

    def _request(self, method: str, path: str, data: Optional[dict] = None) -> dict:
        """Make HTTP request to server."""
        url = f"{self.server_url}{path}"

        if data:
            body = json.dumps(data).encode('utf-8')
            req = urllib.request.Request(url, data=body, method=method)
            req.add_header('Content-Type', 'application/json')
        else:
            req = urllib.request.Request(url, method=method)

        try:
            with urllib.request.urlopen(req, timeout=30) as response:
                return json.loads(response.read().decode('utf-8'))
        except urllib.error.URLError as e:
            print(f"[ContextAdapter] Connection failed: {e.reason}")
            print(f"[ContextAdapter] Is the server running? Start with: codecontext server")
            return {"error": str(e.reason)}
        except Exception as e:
            print(f"[ContextAdapter] Request error: {e}")
            return {"error": str(e)}

    def is_server_running(self) -> bool:
        """Check if the memory server is running."""
        result = self._request("GET", "/health")
        return "error" not in result

    # =========================================================================
    # Queue Methods - Resilient submission when server is down
    # =========================================================================

    def _acquire_lock(self):
        """Acquire file lock for queue operations (cross-platform)."""
        QUEUE_LOCK.parent.mkdir(parents=True, exist_ok=True)
        self._lock_fd = open(QUEUE_LOCK, 'w')

        if sys.platform == 'win32':
            import msvcrt
            msvcrt.locking(self._lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
        else:
            import fcntl
            fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)

    def _release_lock(self):
        """Release file lock."""
        if hasattr(self, '_lock_fd') and self._lock_fd:
            try:
                if sys.platform == 'win32':
                    import msvcrt
                    try:
                        msvcrt.locking(self._lock_fd.fileno(), msvcrt.LK_UNLCK, 1)
                    except:
                        pass
                self._lock_fd.close()
            except:
                pass
            self._lock_fd = None

    def parse_xml_to_memories(self, xml_string: str) -> List[Dict]:
        """
        Parse XML triage output into a list of memory dictionaries.

        Args:
            xml_string: XML string from triage (may have text before/after)

        Returns:
            List of memory dictionaries ready for storage/queuing
        """
        # Extract <learnings> block
        match = re.search(r'<learnings>(.*?)</learnings>', xml_string, re.DOTALL)
        if not match:
            return []

        xml_content = f"<learnings>{match.group(1)}</learnings>"

        # Clean XML
        xml_content = xml_content.replace("```xml", "").replace("```", "")

        try:
            root = ET.fromstring(xml_content)
        except ET.ParseError:
            return []

        # Check for skip element
        skip_elem = root.find("skip")
        if skip_elem is not None:
            return []

        # Collect all learnings
        memories = []
        for learning in root.findall("learning"):
            obs_type = (learning.findtext("type") or "discovery").strip().lower()
            title = (learning.findtext("title") or "").strip()
            fact = (learning.findtext("fact") or "").strip()
            files_text = (learning.findtext("files") or "unknown").strip()

            # Parse files
            if files_text.lower() == "unknown":
                files = []
            else:
                files = [f.strip() for f in files_text.split(",") if f.strip()]

            if title and fact:
                # Validate type
                if obs_type not in self.VALID_TYPES:
                    obs_type = "discovery"

                memories.append({
                    "type": obs_type,
                    "title": title,
                    "fact": fact,
                    "files": files
                })

        return memories

    def queue_memories(self, memories: List[Dict], project: str = None) -> bool:
        """
        Queue memories to disk for later submission when server is available.

        Args:
            memories: List of memory dictionaries to queue
            project: Project name (defaults to self.project)

        Returns:
            True if queued successfully, False otherwise
        """
        if not memories:
            return False

        project = project or self.project

        try:
            self._acquire_lock()
            QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)

            entry = {
                "timestamp": utc_now_iso(),
                "project": project,
                "memories": memories
            }

            with open(QUEUE_FILE, 'a', encoding='utf-8') as f:
                f.write(json.dumps(entry) + '\n')

            print(f"[ContextAdapter] Queued {len(memories)} memories for later")
            return True
        except Exception as e:
            print(f"[ContextAdapter] Failed to queue: {e}")
            return False
        finally:
            self._release_lock()

    def flush_queue(self) -> Dict:
        """
        Attempt to submit all queued memories to the server.

        Returns:
            Dict with stats: processed, succeeded, failed, server_down (optional)
        """
        if not QUEUE_FILE.exists():
            return {"processed": 0, "succeeded": 0, "failed": 0}

        if not self.is_server_running():
            return {"processed": 0, "succeeded": 0, "failed": 0, "server_down": True}

        try:
            self._acquire_lock()

            # Read all entries
            with open(QUEUE_FILE, 'r', encoding='utf-8') as f:
                entries = [json.loads(line) for line in f if line.strip()]

            if not entries:
                return {"processed": 0, "succeeded": 0, "failed": 0}

            succeeded = []
            failed = []

            for entry in entries:
                result = self._request("POST", "/memory/batch", {
                    "memories": entry["memories"],
                    "project": entry["project"]
                })

                if "error" not in result:
                    succeeded.append(entry)
                    count = result.get("count", len(entry["memories"]))
                    print(f"[ContextAdapter] Flushed {count} memories to project '{entry['project']}'")
                else:
                    failed.append(entry)

            # Rewrite queue with only failed entries
            if failed:
                with open(QUEUE_FILE, 'w', encoding='utf-8') as f:
                    for entry in failed:
                        f.write(json.dumps(entry) + '\n')
            else:
                QUEUE_FILE.unlink()  # Delete empty queue

            print(f"[ContextAdapter] Queue flush: {len(succeeded)} succeeded, {len(failed)} remain")
            return {
                "processed": len(entries),
                "succeeded": len(succeeded),
                "failed": len(failed)
            }
        except Exception as e:
            print(f"[ContextAdapter] Queue flush error: {e}")
            return {"error": str(e)}
        finally:
            self._release_lock()

    def has_pending(self) -> int:
        """
        Return count of pending queued memory batches.

        Returns:
            Number of queued batches (not individual memories)
        """
        if not QUEUE_FILE.exists():
            return 0
        try:
            with open(QUEUE_FILE, 'r', encoding='utf-8') as f:
                return sum(1 for line in f if line.strip())
        except:
            return 0

    # =========================================================================
    # Storage Methods
    # =========================================================================

    def store_observation(self, obs_type: str, title: str, fact: str, files: List[str]) -> bool:
        """
        Store a single observation.

        Returns:
            True if stored successfully, False otherwise
        """
        # Validate type
        obs_type = obs_type.lower()
        if obs_type not in self.VALID_TYPES:
            obs_type = "discovery"

        result = self._request("POST", "/memory", {
            "type": obs_type,
            "title": title,
            "fact": fact,
            "files": files,
            "project": self.project
        })

        return "error" not in result

    def store_from_xml(self, xml_string: str) -> int:
        """
        Parse XML triage output and store observations.

        Args:
            xml_string: XML string from triage (may have text before/after)

        Returns:
            Number of observations stored
        """
        # Use shared parsing method
        memories = self.parse_xml_to_memories(xml_string)

        if not memories:
            print("[ContextAdapter] No valid learnings found in XML")
            return 0

        # Use batch endpoint for efficiency
        result = self._request("POST", "/memory/batch", {
            "memories": memories,
            "project": self.project
        })

        if "error" in result:
            print(f"[ContextAdapter] Failed to store: {result['error']}")
            return 0

        count = result.get("count", 0)
        print(f"[ContextAdapter] Stored {count} observations to project '{self.project}'")
        return count

    def get_context(self, query: str, top_k: int = 5) -> str:
        """
        Get context string for injecting into Claude Code prompt.

        Args:
            query: Task/feature description
            top_k: Number of memories to retrieve

        Returns:
            Formatted context string (empty if no relevant memories)
        """
        result = self._request("POST", "/query", {
            "query": query,
            "top_k": top_k,
            "project": self.project
        })

        if "error" in result:
            return ""

        results = result.get("results", [])
        if not results:
            return ""

        lines = [
            "<previous_learnings>",
            "Relevant learnings from previous sessions on this project:",
            ""
        ]

        for r in results:
            # Only include good matches (>30% relevance)
            if r.get("relevance_pct", 0) < 30:
                continue

            files_str = ", ".join(r.get("files", [])) if r.get("files") else "unknown"
            lines.append(f"- [{r['type'].upper()}] {r['title']}")
            lines.append(f"  {r['fact']}")
            lines.append(f"  Files: {files_str}")
            lines.append("")

        if len(lines) <= 3:  # Only header, no content
            return ""

        lines.append("</previous_learnings>")
        lines.append("")
        lines.append("Use these learnings to avoid repeating mistakes and follow established patterns.")

        return "\n".join(lines)

    def count(self) -> int:
        """Return number of memories for this project."""
        result = self._request("GET", f"/stats?project={self.project}")
        if "error" in result:
            return 0
        return result.get("memory_count", 0)


# Test
if __name__ == "__main__":
    print(f"\n{'='*60}")
    print("ContextAdapter Test")
    print(f"{'='*60}\n")

    adapter = ContextAdapter(project="test-project")

    # Check server
    print("Checking server connection...")
    if not adapter.is_server_running():
        print("ERROR: Server not running!")
        print("Start with: codecontext server")
        exit(1)

    print("Server is running!\n")

    # Test XML parsing and storage
    xml_response = '''
Here are the learnings from this session:

<learnings>
  <learning>
    <type>bugfix</type>
    <title>Fixed JWT import error</title>
    <fact>Changed from jwt import token to import jwt because PyJWT uses default export</fact>
    <files>src/auth/jwt.py</files>
  </learning>
  <learning>
    <type>feature</type>
    <title>Implemented refresh tokens</title>
    <fact>Added refresh_token function with 7-day expiry stored in httpOnly cookie</fact>
    <files>src/auth/jwt.py, src/auth/routes.py</files>
  </learning>
</learnings>

Let me know if you need anything else!
'''

    print("Storing from XML...")
    count = adapter.store_from_xml(xml_response)
    print(f"Stored {count} observations\n")

    print("-" * 40)
    print("Getting context for: 'authentication'")
    print("-" * 40)
    context = adapter.get_context("authentication")
    print(context if context else "(no relevant context)")

    print(f"\n{'='*60}")
    print(f"Total memories in project: {adapter.count()}")
    print("SUCCESS!")
    print(f"{'='*60}\n")
