"""Output validation module for verifying generated code was correctly saved.

This module provides functions to validate that the code generated by the LLM
was correctly processed and saved to disk, with proper handling of escape sequences
and other potential issues.
"""

import difflib
import logging
import os
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

logger = logging.getLogger("gollm.validation.output")


def validate_saved_code(
    original_response: str, saved_file_path: str
) -> Tuple[bool, List[str], Dict[str, Any]]:
    """
    Validate that the code generated in the prompt was correctly saved to the file.

    This function compares the original LLM response with the saved file to ensure
    that escape sequences were properly handled and the code was correctly extracted.

    Args:
        original_response: The original response from the LLM
        saved_file_path: Path to the saved file

    Returns:
        Tuple of (is_valid, issues, details) where:
        - is_valid: Boolean indicating if the saved code is valid
        - issues: List of issues found during validation
        - details: Dictionary with additional validation details
    """
    issues = []
    details = {
        "original_length": len(original_response),
        "saved_length": 0,
        "escape_sequences_handled": False,
        "code_blocks_extracted": False,
        "diff_summary": "",
        "escape_sequences_found": {},
    }

    # Check if file exists
    if not os.path.exists(saved_file_path):
        issues.append(f"File {saved_file_path} does not exist")
        return False, issues, details

    # Read the saved file
    try:
        with open(saved_file_path, "r", encoding="utf-8") as f:
            saved_content = f.read()
        details["saved_length"] = len(saved_content)
    except Exception as e:
        issues.append(f"Error reading file {saved_file_path}: {str(e)}")
        return False, issues, details

    # Check for empty file
    if not saved_content or saved_content.isspace():
        issues.append("Saved file is empty or contains only whitespace")
        return False, issues, details

    # Check for escape sequences in the original response
    escape_sequences = {
        "\\n": "newline",
        "\\t": "tab",
        "\\r": "carriage return",
        "\\\\": "backslash",
        '\\"': "double quote",
        "\\'": "single quote",
        "\\u": "unicode",
        "\\x": "hex",
    }

    escape_counts = {}
    for escape, name in escape_sequences.items():
        count = original_response.count(escape)
        if count > 0:
            escape_counts[name] = count

    details["escape_sequences_found"] = escape_counts

    # If escape sequences were found in the original, check if they were handled
    if escape_counts:
        details["escape_sequences_handled"] = True

        # Check for literal escape sequences in the saved file (which would be bad)
        literal_escapes_in_saved = {}
        for escape, name in escape_sequences.items():
            count = saved_content.count(escape)
            if count > 0:
                literal_escapes_in_saved[name] = count
                details["escape_sequences_handled"] = False

        if literal_escapes_in_saved:
            issues.append(
                f"Found unprocessed escape sequences in saved file: {literal_escapes_in_saved}"
            )

    # Check if code blocks were extracted from the original response
    code_block_pattern = r"```(?:\w*)?\n(.+?)(?:\n```|$)"
    code_blocks = re.findall(code_block_pattern, original_response, re.DOTALL)

    if code_blocks:
        details["code_blocks_extracted"] = True

        # Compare the saved content with the code blocks
        best_match = None
        best_ratio = 0

        for block in code_blocks:
            # Clean up the block for comparison
            clean_block = block.strip()

            # Calculate similarity ratio
            ratio = difflib.SequenceMatcher(None, clean_block, saved_content).ratio()
            if ratio > best_ratio:
                best_ratio = ratio
                best_match = clean_block

        details["best_match_ratio"] = best_ratio

        # If the best match is significantly different from the saved content
        if best_ratio < 0.7:  # 70% similarity threshold
            issues.append(
                f"Saved content differs significantly from code blocks in original response (similarity: {best_ratio:.2f})"
            )

    # Generate a diff summary
    if code_blocks:
        best_block = code_blocks[0] if code_blocks else original_response
        diff = list(
            difflib.unified_diff(
                best_block.splitlines(),
                saved_content.splitlines(),
                lineterm="",
                n=3,  # Context lines
            )
        )

        if diff:
            details["diff_summary"] = "\n".join(diff[:20])  # Limit to first 20 lines
            if len(diff) > 20:
                details["diff_summary"] += f"\n... and {len(diff) - 20} more lines"

    # Final validation result
    is_valid = len(issues) == 0

    # Log validation results
    if is_valid:
        logger.info(f"Output validation passed for {saved_file_path}")
        if escape_counts:
            logger.info(f"Successfully handled escape sequences: {escape_counts}")
    else:
        logger.warning(
            f"Output validation failed for {saved_file_path}: {', '.join(issues)}"
        )
        logger.debug(f"Validation details: {details}")

    return is_valid, issues, details


def compare_generated_and_saved(
    original_response: str, saved_file_path: str
) -> Dict[str, Any]:
    """
    Compare the original LLM response with the saved file and return detailed metrics.

    This function provides a more detailed comparison than validate_saved_code,
    focusing on providing metrics and insights rather than just validation.

    Args:
        original_response: The original response from the LLM
        saved_file_path: Path to the saved file

    Returns:
        Dictionary with comparison metrics and details
    """
    metrics = {
        "original_length": len(original_response),
        "saved_length": 0,
        "similarity_ratio": 0.0,
        "escape_sequences": {},
        "code_blocks": [],
        "issues": [],
    }

    # Check if file exists
    if not os.path.exists(saved_file_path):
        metrics["issues"].append(f"File {saved_file_path} does not exist")
        return metrics

    # Read the saved file
    try:
        with open(saved_file_path, "r", encoding="utf-8") as f:
            saved_content = f.read()
        metrics["saved_length"] = len(saved_content)
    except Exception as e:
        metrics["issues"].append(f"Error reading file: {str(e)}")
        return metrics

    # Extract code blocks from the original response
    code_block_pattern = r"```(?:\w*)?\n(.+?)(?:\n```|$)"
    code_blocks = re.findall(code_block_pattern, original_response, re.DOTALL)

    for i, block in enumerate(code_blocks):
        block_metrics = {
            "index": i,
            "length": len(block),
            "similarity_to_saved": difflib.SequenceMatcher(
                None, block, saved_content
            ).ratio(),
        }
        metrics["code_blocks"].append(block_metrics)

    # Find the best matching block
    if code_blocks:
        best_block = max(metrics["code_blocks"], key=lambda x: x["similarity_to_saved"])
        metrics["best_matching_block"] = best_block["index"]
        metrics["similarity_ratio"] = best_block["similarity_to_saved"]
    else:
        # If no code blocks, compare the whole response
        metrics["similarity_ratio"] = difflib.SequenceMatcher(
            None, original_response, saved_content
        ).ratio()

    # Check for escape sequences
    escape_sequences = {
        "\\n": "newline",
        "\\t": "tab",
        "\\r": "carriage return",
        "\\\\": "backslash",
        '\\"': "double quote",
        "\\'": "single quote",
        "\\u": "unicode",
        "\\x": "hex",
    }

    for escape, name in escape_sequences.items():
        original_count = original_response.count(escape)
        saved_count = saved_content.count(escape)

        if original_count > 0 or saved_count > 0:
            metrics["escape_sequences"][name] = {
                "original_count": original_count,
                "saved_count": saved_count,
                "properly_handled": original_count > 0 and saved_count == 0,
            }

    # Check for potential issues
    if metrics["similarity_ratio"] < 0.7:
        metrics["issues"].append(
            f"Low similarity between original and saved content: {metrics['similarity_ratio']:.2f}"
        )

    for name, counts in metrics["escape_sequences"].items():
        if not counts["properly_handled"]:
            metrics["issues"].append(f"Escape sequence '{name}' not properly handled")

    return metrics
