"""
Auto-Resolve Conflicts Module

Uses Claude Code CLI to intelligently resolve memory conflicts.
Spawns Claude CLI as subprocess - no API key needed!

Features tiered processing:
- "auto" confidence: Resolved without LLM (orphaned, clear patterns)
- "high/medium" confidence: Batched for LLM processing
- "low" confidence: Processed with full attention
"""

import asyncio
import os
import shutil
import subprocess
import sys
import re
from typing import List, Dict, Any, Optional, Tuple

from ..cli_utils import get_subprocess_env, get_session_dir, build_claude_cli_args


# Maximum conflicts to process per batch (to avoid overwhelming the LLM)
MAX_BATCH_SIZE = 10


def separate_by_confidence(conflicts: List[Dict]) -> Tuple[List[Dict], List[Dict], List[Dict]]:
    """
    Separate conflicts into tiers by confidence level.

    All conflicts now go to LLM - no auto-processing.
    Length-based heuristics are unreliable: shorter memories can be more
    accurate/current, longer ones can be verbose but less useful.

    Returns:
        Tuple of (auto_resolvable, llm_batch, low_confidence)
        - auto_resolvable: Always empty (no auto-processing)
        - llm_batch: All conflicts go here for LLM decision
        - low_confidence: Always empty (merged into llm_batch)
    """
    # Route everything to LLM - no auto-processing
    return [], conflicts, []


def auto_resolve_without_llm(conflicts: List[Dict]) -> List[Dict]:
    """
    Resolve conflicts that don't need LLM processing.

    These are conflicts where the existing memory has minimal content (< 10 chars),
    making the decision obvious: keep the more detailed NEW memory.

    With the snapshot architecture, we always have the existing content stored,
    so this is purely based on content quality, not missing data.
    """
    resolutions = []

    for c in conflicts:
        existing_snapshot = c.get("conflict_with") or c.get("existing_snapshot") or {}
        existing_fact = existing_snapshot.get("fact", "") or ""

        resolutions.append({
            "conflict_id": c.get("id"),
            "decision": "keep_new",
            "reasoning": f"Auto-resolved: existing memory has minimal content ({len(existing_fact)} chars)",
            "auto_resolved": True
        })

    return resolutions


async def resolve_single_conflict(conflict: Dict, store) -> Dict:
    """
    Resolve a single conflict immediately (real-time mode).

    This function is called when auto-resolve is enabled and a new
    conflict is detected during memory add. It resolves the conflict
    inline without requiring user interaction.

    Args:
        conflict: The conflict dictionary with id, type, title, fact, etc.
        store: The ContextStore instance to apply the resolution to.

    Returns:
        Dict with success status, decision, reasoning, and resolution result.
    """
    # Use existing tier logic to decide approach
    auto_resolvable, llm_batch, _ = separate_by_confidence([conflict])

    resolutions = []
    if auto_resolvable:
        # No LLM needed - auto-resolve based on content quality
        resolutions = auto_resolve_without_llm(auto_resolvable)
    elif llm_batch or _:  # llm_batch or low_confidence
        # Need LLM to decide
        conflicts_to_process = llm_batch + _
        try:
            # Extract project from conflict for session logs directory
            project = conflict.get("project", "_global")
            prompt = build_resolution_prompt(conflicts_to_process)
            response = await spawn_claude_code(prompt, project=project)
            resolutions = parse_resolution_response(response)
            for r in resolutions:
                r["auto_resolved"] = False
        except Exception as e:
            # On LLM failure, default to keep_both (safe)
            resolutions = [{
                "conflict_id": conflict.get("id"),
                "decision": "keep_both",
                "reasoning": f"LLM unavailable, defaulting to keep_both: {str(e)[:50]}",
                "auto_resolved": True,
                "error": True
            }]

    if not resolutions:
        return {"success": False, "error": "No resolution returned"}

    resolution = resolutions[0]
    conflict_id = resolution.get("conflict_id")
    decision = resolution.get("decision")
    merged_fact = resolution.get("merged_fact")

    # Apply the resolution to the store
    try:
        result = store.resolve_conflict(
            conflict_id=conflict_id,
            resolution=decision,
            merged_fact=merged_fact
        )
        return {
            "success": result.get("success", False),
            "conflict_id": conflict_id,
            "decision": decision,
            "reasoning": resolution.get("reasoning", ""),
            "auto_resolved": resolution.get("auto_resolved", False),
            **result
        }
    except Exception as e:
        return {
            "success": False,
            "conflict_id": conflict_id,
            "decision": decision,
            "error": str(e)
        }


def build_resolution_prompt(conflicts: List[Dict[str, Any]]) -> str:
    """Build the prompt for Claude to resolve conflicts."""

    # Format each conflict with file overlap calculation
    formatted_conflicts = []
    for i, c in enumerate(conflicts, 1):
        conflict_with = c.get("conflict_with", {}) or {}

        # Calculate file overlap for better decision-making
        new_files = set(c.get('files', []) or [])
        existing_files = set(conflict_with.get('files', []) or [])
        file_overlap = len(new_files & existing_files)
        total_files = len(new_files | existing_files)

        formatted = f"""--- CONFLICT {i} ---
ID: {c.get('id', 'unknown')}
Type: {c.get('type', 'unknown')}
File Overlap: {file_overlap}/{total_files} files shared

NEW MEMORY (created more recently):
  Title: {c.get('title', 'N/A')}
  Fact: {c.get('fact', 'N/A')}
  Files: {', '.join(c.get('files', [])) or 'None'}

EXISTING MEMORY (created earlier):
  Title: {conflict_with.get('title', 'N/A')}
  Fact: {conflict_with.get('fact', 'N/A')}
  Files: {', '.join(conflict_with.get('files', [])) or 'None'}
"""
        formatted_conflicts.append(formatted)

    conflicts_text = "\n".join(formatted_conflicts)

    prompt = f"""IMPORTANT: Output ONLY valid XML. No explanations, no markdown, no insight blocks.

You are resolving conflicts in a coding knowledge memory store.
Each conflict has a NEW memory and an EXISTING memory that are semantically similar.

========================================
FILE ANALYSIS (check this FIRST)
========================================

The "File Overlap" metric shows how many files are shared between memories.

**0 shared files** = Check if this is plan-to-implementation (see LIFECYCLE below)
  OR truly different code areas (keep_both)

**Some shared files** = Memories may be about the same code
  Could be keep_new (update), merge, or keep_both depending on content

**All files shared** = Memories are about the exact same code
  Likely keep_new (supersedes) or merge (combine details)

========================================
LIFECYCLE PROGRESSION (plan -> implementation)
========================================

IMPORTANT: When files have 0 overlap, check if this is plan-to-implementation:

**Pattern to detect**:
- EXISTING references a plan/design document (.md file in docs/, plans/, future-plans/)
- NEW references actual code files (.py, .html, .js, .css, etc.)
- Both describe the SAME feature at different lifecycle stages

This is NOT "different code areas" - it's the SAME feature evolving.

Resolution: Usually **merge** is best - combines planning context with implementation details.

========================================
EVOLUTION DETECTION
========================================

Look for signs that NEW **supersedes** or **extends** EXISTING:

**Version indicators** (NEW should usually win or merge):
- NEW mentions "v2", "updated", "improved", "new approach"
- NEW describes a different implementation strategy for same goal
- NEW references the same plan/design doc with implementation details

**Extension indicators** (usually keep_both or merge):
- NEW adds information EXISTING doesn't have
- NEW describes additional capabilities or features

========================================
DECISION CRITERIA
========================================

**merge** - Choose when (PREFERRED for same-feature conflicts):
- **Plan->Implementation**: EXISTING is a plan (.md), NEW is implementation (code files)
- Both describe the SAME feature/concept with complementary details
- Combining creates ONE superior memory without losing information
- One describes "why/planning", the other describes "what/how"
- There's significant overlap AND each has unique details

**keep_both** - Choose when:
- Files are DIFFERENT AND they describe truly different code areas
- They describe DIFFERENT aspects of the same topic
- Both have UNIQUE valuable information not in the other
- You're unsure AND it's not a plan->implementation case

**keep_new** - Choose when:
- NEW **supersedes** EXISTING (same topic, newer/better approach)
- NEW has more specific details AND covers everything EXISTING does
- NEW corrects outdated/incorrect information in EXISTING
- NEW is clearly the "v2" or updated version

**keep_existing** - Choose when:
- NEW is truly redundant (says the same thing with fewer details)
- NEW is a summary of what EXISTING already explains in depth
- EXISTING genuinely has MORE useful information than NEW
- Be careful: Don't choose this just because EXISTING is longer

========================================
COMMON MISTAKES TO AVOID
========================================

DON'T choose keep_existing just because:
- EXISTING mentions more file paths or folder names
- EXISTING is longer or more verbose
- EXISTING has "implementation details" but NEW has the actual approach

DON'T treat plan->implementation as "different areas":
- .md plan file + .py/.html implementation = SAME feature = merge

DO prefer merge when:
- Same feature at different lifecycle stages
- Complementary details about same concept

========================================
EXAMPLES
========================================

**Example 1: Plan->Implementation = merge**
EXISTING: "Designing debug mode toggle per cli-session-management.md"
  Files: future-plans/cli-session-management.md
NEW: "Implemented debug mode toggle with UI checkbox"
  Files: templates/index.html, static/style.css
File Overlap: 0/3
-> merge (same feature: plan + implementation = one comprehensive memory)

**Example 2: Different code areas = keep_both**
NEW: "Fixed authentication bug in login handler"
  Files: auth/login.py
EXISTING: "Database connection pooling configuration"
  Files: db/config.py
File Overlap: 0/2
-> keep_both (truly different areas, no semantic connection)

**Example 3: NEW supersedes = keep_new**
NEW: "Fixed null pointer in getUserById() when user ID is undefined"
EXISTING: "Fixed a bug in the API"
File Overlap: 1/1
-> keep_new (NEW is more specific, EXISTING is vague)

**Example 4: Same concept, complementary = merge**
EXISTING: "Use Redis for caching API responses"
NEW: "Redis cache configured with 5-minute TTL"
File Overlap: 1/1
-> merge: "Use Redis for caching API responses with 5-minute TTL"

**Example 5: True redundancy = keep_existing**
EXISTING: "LanceDB doesn't support ALTER TABLE - must export to DataFrame, add columns, drop and recreate"
NEW: "LanceDB schema changes require table recreation"
File Overlap: 1/1
-> keep_existing (NEW is just a summary of EXISTING)

========================================
OUTPUT FORMAT (strict XML only)
========================================

<resolutions>
  <resolution>
    <conflict_id>exact-id-from-input</conflict_id>
    <decision>merge</decision>
    <merged_fact>Combined fact capturing both plan context and implementation details</merged_fact>
    <reasoning>Plan .md + implementation files = same feature lifecycle</reasoning>
  </resolution>
</resolutions>

========================================
RULES
========================================
1. decision MUST be: keep_new | keep_existing | keep_both | merge
2. merged_fact REQUIRED for "merge", OMIT otherwise
3. reasoning: 1 brief sentence
4. conflict_id must match EXACTLY
5. Resolve EVERY conflict
6. Output ONLY the XML block
7. When uncertain between keep_both and keep_existing, prefer keep_both
8. When plan .md + code files for same feature, prefer merge

========================================
CONFLICTS TO RESOLVE
========================================

{conflicts_text}"""

    return prompt


def parse_resolution_response(response: str) -> List[Dict[str, Any]]:
    """Parse the XML response from Claude into resolution decisions."""

    resolutions = []

    # Strip markdown code blocks if present (```xml ... ``` or ``` ... ```)
    cleaned = response
    code_block_match = re.search(r'```(?:xml)?\s*(.*?)```', response, re.DOTALL)
    if code_block_match:
        cleaned = code_block_match.group(1)

    # Extract the <resolutions>...</resolutions> block
    match = re.search(r'<resolutions>(.*?)</resolutions>', cleaned, re.DOTALL)
    if not match:
        # Try to find individual resolution tags
        resolution_matches = re.findall(r'<resolution>(.*?)</resolution>', cleaned, re.DOTALL)
        if not resolution_matches:
            raise ValueError("No valid resolution XML found in response")
    else:
        resolution_matches = re.findall(r'<resolution>(.*?)</resolution>', match.group(1), re.DOTALL)

    for res_xml in resolution_matches:
        resolution = {}

        # Extract conflict_id
        id_match = re.search(r'<conflict_id>(.*?)</conflict_id>', res_xml, re.DOTALL)
        if id_match:
            resolution['conflict_id'] = id_match.group(1).strip()

        # Extract decision
        decision_match = re.search(r'<decision>(.*?)</decision>', res_xml, re.DOTALL)
        if decision_match:
            resolution['decision'] = decision_match.group(1).strip()

        # Extract merged_fact (optional, only for merge decisions)
        merged_match = re.search(r'<merged_fact>(.*?)</merged_fact>', res_xml, re.DOTALL)
        if merged_match:
            resolution['merged_fact'] = merged_match.group(1).strip()

        # Extract reasoning (optional)
        reasoning_match = re.search(r'<reasoning>(.*?)</reasoning>', res_xml, re.DOTALL)
        if reasoning_match:
            resolution['reasoning'] = reasoning_match.group(1).strip()

        # Validate required fields
        if resolution.get('conflict_id') and resolution.get('decision'):
            # Validate decision value
            if resolution['decision'] in ('keep_new', 'keep_existing', 'keep_both', 'merge'):
                resolutions.append(resolution)

    return resolutions


async def spawn_claude_code(prompt: str, project: str = "_global", timeout: int = 120) -> str:
    """
    Spawn Claude Code CLI as subprocess and get response.

    Args:
        prompt: The prompt to send to Claude
        project: Project name for session logs directory (default: "_global")
        timeout: Timeout in seconds
    """

    # Find claude executable (works on Windows and Unix)
    claude_exe = shutil.which("claude")
    if not claude_exe:
        # Try common Windows locations
        import os
        possible_paths = [
            os.path.expanduser("~/.claude/local/claude.exe"),
            os.path.expanduser("~/AppData/Local/Programs/claude/claude.exe"),
            "C:/Program Files/Claude/claude.exe",
        ]
        for path in possible_paths:
            if os.path.exists(path):
                claude_exe = path
                break

    if not claude_exe:
        raise RuntimeError(
            "Claude Code CLI not found in PATH. "
            "Please ensure Claude Code is installed and accessible."
        )

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

    # Spawn Claude Code with --print flag for non-interactive output
    # --dangerously-skip-permissions: Skip permission prompts (headless)
    # --model haiku: Use Haiku for faster, cheaper resolution
    # creationflags: Prevent console window on Windows
    kwargs = {
        "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

    cli_args = build_claude_cli_args(prompt, model="haiku")
    process = await asyncio.create_subprocess_exec(
        claude_exe,
        *cli_args,
        **kwargs
    )

    try:
        stdout, stderr = await asyncio.wait_for(
            process.communicate(),
            timeout=timeout
        )
    except asyncio.TimeoutError:
        process.kill()
        await process.wait()
        raise RuntimeError(f"Claude Code CLI timed out after {timeout} seconds")

    if process.returncode != 0:
        error_msg = stderr.decode('utf-8', errors='replace') if stderr else "Unknown error"
        raise RuntimeError(f"Claude Code CLI failed: {error_msg}")

    return stdout.decode('utf-8', errors='replace')


async def auto_resolve_conflicts(
    conflicts: List[Dict[str, Any]],
    max_batch: int = MAX_BATCH_SIZE
) -> Dict[str, Any]:
    """
    Send conflicts to Claude Code CLI for intelligent resolution.

    Uses tiered processing:
    1. Auto-resolve: "auto" confidence + orphaned (no LLM needed)
    2. LLM batch: "high" and "medium" confidence
    3. Low confidence: Processed with full attention

    Args:
        conflicts: List of conflict dictionaries from the store
        max_batch: Maximum conflicts to process (default 10)

    Returns:
        Dict with 'resolutions' list, 'processed' count, and tier stats
    """

    if not conflicts:
        return {"resolutions": [], "processed": 0, "total": 0}

    total = len(conflicts)

    # Separate conflicts by confidence tier
    auto_resolvable, llm_batch, low_confidence = separate_by_confidence(conflicts)

    # Start with auto-resolved conflicts (no LLM needed)
    resolutions = auto_resolve_without_llm(auto_resolvable)
    auto_count = len(resolutions)

    # Combine high/medium + low for LLM processing, respecting batch limit
    llm_conflicts = llm_batch + low_confidence
    remaining_budget = max_batch - auto_count

    if remaining_budget > 0 and llm_conflicts:
        # Take up to remaining budget
        llm_to_process = llm_conflicts[:remaining_budget]

        # Build prompt and call LLM
        print(f"[AutoResolve] Processing {len(llm_to_process)} conflicts via LLM...")
        prompt = build_resolution_prompt(llm_to_process)
        print(f"[AutoResolve] Prompt built ({len(prompt)} chars), calling Claude CLI...")

        # Extract project from first conflict for session logs directory
        project = llm_to_process[0].get("project", "_global") if llm_to_process else "_global"

        try:
            response = await spawn_claude_code(prompt, project=project)
            print(f"[AutoResolve] Claude CLI returned {len(response)} chars")
            llm_resolutions = parse_resolution_response(response)
            print(f"[AutoResolve] Parsed {len(llm_resolutions)} resolutions")

            # Mark LLM-resolved conflicts
            for r in llm_resolutions:
                r["auto_resolved"] = False

            resolutions.extend(llm_resolutions)
        except Exception as e:
            # If LLM fails, mark remaining as needing manual review
            import traceback
            print(f"[AutoResolve] LLM call failed: {e}")
            print(f"[AutoResolve] Traceback: {traceback.format_exc()}")
            for c in llm_to_process:
                resolutions.append({
                    "conflict_id": c.get("id"),
                    "decision": "keep_both",  # Safe default
                    "reasoning": f"LLM processing failed, defaulting to keep_both: {str(e)[:50]}",
                    "auto_resolved": True,
                    "error": True
                })

    # Calculate stats
    processed = len(resolutions)
    llm_processed = processed - auto_count

    return {
        "resolutions": resolutions,
        "processed": processed,
        "total": total,
        "remaining": max(0, total - processed),
        # Tier statistics
        "auto_resolved_count": auto_count,
        "llm_resolved_count": llm_processed,
        "high_confidence_count": len([c for c in llm_batch if c.get("confidence") == "high"]),
        "medium_confidence_count": len([c for c in llm_batch if c.get("confidence") == "medium"]),
        "low_confidence_count": len(low_confidence),
        # Cost savings indicator
        "llm_calls_saved": auto_count
    }


# For testing
if __name__ == "__main__":
    import json

    # Test conflicts
    test_conflicts = [
        {
            "id": "test-conflict-1",
            "type": "discovery",
            "title": "LanceDB uses L2 distance for similarity",
            "fact": "LanceDB calculates similarity using L2 (Euclidean) distance by default.",
            "files": ["store.py"],
            "conflict_with": {
                "title": "Vector similarity in LanceDB",
                "fact": "LanceDB supports multiple distance metrics including L2 and cosine.",
                "files": ["store.py"]
            }
        }
    ]

    async def test():
        result = await auto_resolve_conflicts(test_conflicts)
        print(json.dumps(result, indent=2))

    asyncio.run(test())
