"""
CodeContext Summary Module - Evolving project summaries for session injection.

This module handles:
- Summary generation via Claude CLI
- Summary storage (JSON for current, LanceDB for history)
- Timeline formatting for injection
- Version management (use old versions, rollback)
"""

import asyncio
import json
import os
import re
import shutil
import subprocess
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional, TYPE_CHECKING

from ..cli_utils import get_subprocess_env, get_session_dir, build_claude_cli_args
from ..utils.timestamps import compare_timestamps, utc_now_iso

if TYPE_CHECKING:
    from ..memory_store import ContextStore


# Emoji mapping for memory types
EMOJI_MAP = {
    "bugfix": "\U0001F41B",      # Bug emoji
    "feature": "\u2728",         # Sparkles
    "discovery": "\U0001F4A1",   # Light bulb
    "decision": "\U0001F3AF",    # Target/bullseye
    "refactor": "\U0001F3D7\uFE0F",  # Building construction
    "optimization": "\u26A1",    # Lightning bolt
    "gotcha": "\u26A0\uFE0F",    # Warning sign
}
DEFAULT_EMOJI = "\U0001F4CC"  # Pushpin


# Summary generation prompt template
SUMMARY_PROMPT = '''You are a project summarizer. Create a concise, evolved summary of this coding project.

INSTRUCTIONS:
1. If no previous summary, create a fresh project summary
2. If previous summary exists, EVOLVE it with the new information
3. Reflect any changes in direction (tech stack changes, architectural shifts)
4. Prioritize recent information over older conflicting info
5. Keep it concise (~800-1000 words)
6. Include: tech stack, architecture decisions, recent focus areas, known gotchas

PREVIOUS SUMMARY (v{version}):
<previous_summary>
{previous_summary}
</previous_summary>

NEW MEMORIES SINCE LAST SUMMARY (newest first):
<new_memories>
{new_memories}
</new_memories>

OUTPUT FORMAT:
Return ONLY the XML below with your summary inside. No other text.

<new_summary>
Your summary text here. Write in clear paragraphs covering:
- Project overview and purpose
- Current tech stack and architecture
- Recent development focus
- Key decisions made
- Known issues and gotchas
</new_summary>'''


def estimate_tokens(text: str) -> int:
    """
    Rough token estimation: 4 characters ~ 1 token.

    Args:
        text: Input text

    Returns:
        Estimated token count
    """
    return len(text) // 4


def get_emoji(memory_type: str) -> str:
    """Get emoji for a memory type."""
    return EMOJI_MAP.get(memory_type, DEFAULT_EMOJI)


def format_memory_for_timeline(memory: dict) -> str:
    """
    Format a single memory for timeline display.

    Args:
        memory: Memory dict with type, title, created_at

    Returns:
        Formatted string like "[Jan 22, 10:30] Bug Fix: Fixed timezone"
    """
    created_at = memory.get("created_at", "")
    mem_type = memory.get("type", "discovery")
    title = memory.get("title", "Untitled")
    emoji = get_emoji(mem_type)

    # Parse and format timestamp
    date_str = ""
    if created_at:
        try:
            # Parse ISO format
            if "T" in created_at:
                dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
            else:
                dt = datetime.fromisoformat(created_at)
            date_str = dt.strftime("%b %d, %H:%M")
        except (ValueError, AttributeError):
            date_str = created_at[:16] if len(created_at) >= 16 else created_at

    return f"[{date_str}] {emoji} {title}"


def format_memory_for_prompt(memory: dict, index: int) -> str:
    """
    Format a memory for the summary generation prompt.

    Args:
        memory: Memory dict with type, title, fact, created_at
        index: Position in the list (1-based)

    Returns:
        Formatted string for prompt
    """
    created_at = memory.get("created_at", "")
    mem_type = memory.get("type", "discovery")
    title = memory.get("title", "Untitled")
    fact = memory.get("fact", "")

    # Parse timestamp
    date_str = ""
    if created_at:
        try:
            if "T" in created_at:
                dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
            else:
                dt = datetime.fromisoformat(created_at)
            date_str = dt.strftime("%Y-%m-%d %H:%M")
        except (ValueError, AttributeError):
            date_str = created_at[:16] if len(created_at) >= 16 else created_at

    return f"[{index}] {date_str} | {mem_type} | {title}\n    {fact}"


class SummaryManager:
    """
    Manages project summaries: generation, storage, retrieval, versioning.

    Each project has:
    - A current summary stored as JSON file
    - A history of summaries stored in LanceDB
    """

    def __init__(self, base_path: Path, project: str, store: Optional["ContextStore"] = None):
        """
        Initialize summary manager for a project.

        Args:
            base_path: Base path for CodeContext data (~/.codecontext/)
            project: Project name (encoded, e.g., C--Users-Sam-my-app)
            store: Optional ContextStore for memory queries (can be set later)
        """
        from cli_utils import PROJECTS_DIR

        self.base_path = base_path
        self.project = project
        self.store = store
        # NEW: Use PROJECTS_DIR for project-specific data
        self.project_path = PROJECTS_DIR / project
        self.summary_file = self.project_path / "summary.json"

        # Ensure project directory exists
        self.project_path.mkdir(parents=True, exist_ok=True)

    def set_store(self, store: "ContextStore"):
        """Set the context store (for lazy initialization)."""
        self.store = store

    def load_current(self) -> Optional[dict]:
        """
        Load the current summary from JSON file.

        Returns:
            Summary dict or None if not found
        """
        if not self.summary_file.exists():
            return None

        try:
            with open(self.summary_file, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            print(f"[Summary:{self.project}] Error loading summary: {e}")
            return None

    def save_current(self, summary_data: dict) -> bool:
        """
        Save the current summary to JSON file.

        Args:
            summary_data: Summary dict to save

        Returns:
            True if successful
        """
        try:
            with open(self.summary_file, "w", encoding="utf-8") as f:
                json.dump(summary_data, f, indent=2)
            return True
        except IOError as e:
            print(f"[Summary:{self.project}] Error saving summary: {e}")
            return False

    def add_to_history(self, summary_data: dict) -> Optional[str]:
        """
        Add a summary version to LanceDB history.

        Args:
            summary_data: Summary dict to add to history

        Returns:
            History record ID or None if failed
        """
        if self.store is None:
            print(f"[Summary:{self.project}] No store available for history")
            return None

        try:
            result = self.store.add_summary_version(summary_data)
            return result.get("id")
        except Exception as e:
            print(f"[Summary:{self.project}] Error adding to history: {e}")
            return None

    def get_history(self, limit: int = 50) -> List[dict]:
        """
        Get summary version history from LanceDB.

        Args:
            limit: Maximum number of versions to return

        Returns:
            List of summary history records (newest first)
        """
        if self.store is None:
            return []

        try:
            return self.store.get_summary_history(limit)
        except Exception as e:
            print(f"[Summary:{self.project}] Error getting history: {e}")
            return []

    def get_version(self, version: int) -> Optional[dict]:
        """
        Get a specific summary version from history.

        Args:
            version: Version number to retrieve

        Returns:
            Summary record or None if not found
        """
        if self.store is None:
            return None

        try:
            return self.store.get_summary_version(version)
        except Exception as e:
            print(f"[Summary:{self.project}] Error getting version {version}: {e}")
            return None

    def use_version(self, version: int) -> bool:
        """
        Set an old version as the current summary.

        This will:
        1. Load the old version from history
        2. Save current to history (as a new version)
        3. Set the old version as current

        Args:
            version: Version number to use

        Returns:
            True if successful
        """
        old_version = self.get_version(version)
        if not old_version:
            print(f"[Summary:{self.project}] Version {version} not found")
            return False

        current = self.load_current()

        # Save current to history before replacing
        if current:
            self.add_to_history(current)

        # Create new current from old version
        new_current = {
            "project": self.project,
            "version": (current.get("version", 0) if current else 0) + 1,
            "summary": old_version.get("summary", ""),
            "generated_at": utc_now_iso(),
            "memory_count": self.store.count() if self.store else 0,
            "last_memory_timestamp": current.get("last_memory_timestamp", "") if current else "",
            "pending": False,
            "trigger": "rollback"
        }

        return self.save_current(new_current)

    def get_new_memories_since(self, timestamp: str) -> List[dict]:
        """
        Get memories created after the given timestamp.

        Args:
            timestamp: ISO timestamp to filter from

        Returns:
            List of memories (newest first)
        """
        if self.store is None:
            return []

        try:
            # Get all memories and filter by timestamp
            all_memories = self.store.list_all(limit=1000, offset=0, sort_order="newest")

            if not timestamp:
                return all_memories

            new_memories = []
            for m in all_memories:
                created_at = m.get("created_at", "")
                # Use safe comparison that handles both naive and timezone-aware formats
                if compare_timestamps(created_at, timestamp) > 0:
                    new_memories.append(m)

            return new_memories
        except Exception as e:
            print(f"[Summary:{self.project}] Error getting new memories: {e}")
            return []

    def _find_claude_cli(self) -> Optional[str]:
        """
        Find the Claude CLI executable.

        Uses same logic as resolve.py for consistency.

        Returns:
            Path to Claude CLI or None if not found
        """
        # Try PATH first
        claude_exe = shutil.which("claude")
        if claude_exe:
            return claude_exe

        # Common Windows locations (same as extract.py)
        home = Path.home()
        candidates = [
            home / ".local" / "bin" / "claude.exe",
            home / ".local" / "bin" / "claude",
            home / "AppData" / "Local" / "Programs" / "claude" / "claude.exe",
            Path("C:/Program Files/claude/claude.exe"),
            Path("C:/Program Files (x86)/claude/claude.exe"),
        ]
        for candidate in candidates:
            if candidate.exists():
                return str(candidate)

        return None

    async def generate_summary(
        self,
        previous_summary: Optional[str] = None,
        new_memories: Optional[List[dict]] = None,
        model: str = "haiku",
        max_tokens: int = 2000
    ) -> Optional[str]:
        """
        Generate a new summary using Claude CLI.

        Args:
            previous_summary: Previous summary text (or None for fresh start)
            new_memories: List of new memories to incorporate
            model: Claude model to use (default: haiku for speed)
            max_tokens: Max output tokens

        Returns:
            Generated summary text or None if failed
        """
        import sys

        if not new_memories:
            new_memories = []

        # Find Claude CLI (same logic as resolve.py)
        claude_exe = self._find_claude_cli()
        if not claude_exe:
            print(f"[Summary:{self.project}] Claude CLI not found in PATH or common locations")
            return None

        # Get current version number
        current = self.load_current()
        version = (current.get("version", 0) if current else 0)

        # Format memories for prompt
        formatted_memories = "\n".join(
            format_memory_for_prompt(m, i + 1)
            for i, m in enumerate(new_memories)
        )

        # Build the prompt
        prompt = SUMMARY_PROMPT.format(
            version=version,
            previous_summary=previous_summary or "(No previous summary - this is a fresh project)",
            new_memories=formatted_memories or "(No new memories)"
        )

        try:
            # Get dedicated session directory for this script
            # NEW: Uses get_session_dir with new naming convention
            session_logs_dir = get_session_dir(self.project, "summary_generator")

            # Build subprocess kwargs
            # Use stdin to pass prompt (avoids Windows command line length limit)
            kwargs = {
                "stdin": asyncio.subprocess.PIPE,
                "stdout": asyncio.subprocess.PIPE,
                "stderr": asyncio.subprocess.PIPE,
                "env": get_subprocess_env(),
                "cwd": str(session_logs_dir),  # Isolate sessions from user's project
            }

            # Add Windows-specific flag to hide console window
            if sys.platform == 'win32':
                kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW

            # Call Claude CLI (async) - prompt passed via stdin, not -p flag
            cli_args = build_claude_cli_args("-", model=model, use_stdin=True)
            process = await asyncio.create_subprocess_exec(
                claude_exe,
                *cli_args,
                **kwargs
            )

            # Pass prompt via stdin (avoids WinError 206: filename too long)
            stdout, stderr = await asyncio.wait_for(
                process.communicate(input=prompt.encode('utf-8')),
                timeout=120  # 2 minute timeout
            )

            if process.returncode != 0:
                print(f"[Summary:{self.project}] Claude CLI error: {stderr.decode()[:200]}")
                return None

            output = stdout.decode().strip()

            # Extract summary from XML tags
            match = re.search(r"<new_summary>(.*?)</new_summary>", output, re.DOTALL)
            if match:
                return match.group(1).strip()

            # If no XML tags, try to use the whole output
            print(f"[Summary:{self.project}] Warning: No XML tags in output, using raw output")
            return output

        except asyncio.TimeoutError:
            print(f"[Summary:{self.project}] Claude CLI timeout")
            return None
        except FileNotFoundError as e:
            print(f"[Summary:{self.project}] Claude CLI not found at: {claude_exe}")
            print(f"[Summary:{self.project}] Error details: {e}")
            return None
        except Exception as e:
            print(f"[Summary:{self.project}] Error calling Claude CLI: {e}")
            return None

    async def generate_and_save(
        self,
        trigger: str = "session_end",
        model: str = "haiku",
        force_full: bool = False
    ) -> Optional[dict]:
        """
        Generate a new summary and save it.

        This is the main entry point for summary generation.

        Args:
            trigger: What triggered generation ("session_end", "manual", "scheduled")
            model: Claude model to use
            force_full: If True, regenerate from all memories (not incremental)

        Returns:
            New summary data dict or None if failed/skipped
        """
        current = self.load_current()

        # Get new memories since last summary
        last_timestamp = ""
        if current and not force_full:
            last_timestamp = current.get("last_memory_timestamp", "")

        new_memories = self.get_new_memories_since(last_timestamp)

        # Skip if no new memories (unless forced)
        if not new_memories and not force_full:
            print(f"[Summary:{self.project}] No new memories, skipping generation")
            return None

        # Get previous summary text
        previous_summary = current.get("summary", "") if current else None

        # Mark as pending before generation
        if current:
            current["pending"] = True
            self.save_current(current)

        # Generate new summary
        new_summary_text = await self.generate_summary(
            previous_summary=previous_summary,
            new_memories=new_memories if not force_full else self.store.list_all(limit=1000) if self.store else [],
            model=model
        )

        if not new_summary_text:
            # Generation failed, leave pending flag
            print(f"[Summary:{self.project}] Generation failed, marked as pending")
            return None

        # Save current to history before updating
        if current:
            self.add_to_history(current)

        # Get latest memory timestamp
        latest_timestamp = ""
        if new_memories:
            latest_timestamp = new_memories[0].get("created_at", "")
        elif self.store:
            all_memories = self.store.list_all(limit=1, offset=0, sort_order="newest")
            if all_memories:
                latest_timestamp = all_memories[0].get("created_at", "")

        # Create new summary data
        new_version = (current.get("version", 0) if current else 0) + 1
        memory_count = self.store.count() if self.store else 0

        new_summary_data = {
            "project": self.project,
            "version": new_version,
            "summary": new_summary_text,
            "generated_at": utc_now_iso(),
            "memory_count": memory_count,
            "last_memory_timestamp": latest_timestamp,
            "pending": False,
            "trigger": trigger,
            "new_memories_count": len(new_memories)
        }

        # Save new current
        if self.save_current(new_summary_data):
            print(f"[Summary:{self.project}] Generated v{new_version} ({len(new_memories)} new memories)")
            return new_summary_data

        return None

    def format_timeline(self, limit: int = 10) -> str:
        """
        Format recent memories as a timeline for injection.

        Args:
            limit: Number of recent memories to include

        Returns:
            Formatted timeline string
        """
        if self.store is None:
            return "(No timeline available - store not connected)"

        memories = self.store.list_all(limit=limit, offset=0, sort_order="newest")

        if not memories:
            return "(No memories yet)"

        lines = [format_memory_for_timeline(m) for m in memories]
        return "\n".join(lines)

    def format_injection(self, timeline_limit: int = 10) -> str:
        """
        Format the full injection text (summary + timeline).

        This is what gets injected at session start.

        Args:
            timeline_limit: Number of timeline items

        Returns:
            Full injection text
        """
        current = self.load_current()

        # Header
        parts = []

        if current:
            version = current.get("version", "?")
            parts.append(f"PROJECT SUMMARY (v{version})")
            parts.append("")
            parts.append(current.get("summary", "(No summary text)"))
        else:
            parts.append("PROJECT SUMMARY")
            parts.append("")
            parts.append("(No summary available yet - will be generated at session end)")

        parts.append("")
        parts.append("RECENT ACTIVITY")
        parts.append("-" * 50)
        parts.append(self.format_timeline(timeline_limit))
        parts.append("")
        parts.append("For specific context, use `query_memories` tool.")

        return "\n".join(parts)

    def get_stats(self) -> dict:
        """
        Get summary statistics.

        Returns:
            Dict with version, token count, etc.
        """
        current = self.load_current()

        if not current:
            return {
                "has_summary": False,
                "version": 0,
                "token_count": 0,
                "pending": False
            }

        summary_text = current.get("summary", "")

        return {
            "has_summary": True,
            "version": current.get("version", 0),
            "token_count": estimate_tokens(summary_text),
            "pending": current.get("pending", False),
            "generated_at": current.get("generated_at", ""),
            "memory_count": current.get("memory_count", 0),
            "trigger": current.get("trigger", "")
        }


def calculate_total_tokens(memories: List[dict]) -> int:
    """
    Calculate total token count for a list of memories.

    Args:
        memories: List of memory dicts

    Returns:
        Estimated total token count
    """
    total = 0
    for m in memories:
        # Count title + fact + files
        text = f"{m.get('title', '')} {m.get('fact', '')} {' '.join(m.get('files', []))}"
        total += estimate_tokens(text)
    return total
