#!/usr/bin/env python3

import difflib
import hashlib
import logging
import math
import os
import re
from difflib import SequenceMatcher
from typing import Any, Dict, List, Optional, Tuple

from ..code_command import run_formatter_without_commit
from ..common import get_edit_snippet, normalize_file_path
from ..file_utils import (
    async_open_text,
    check_file_path_and_permissions,
    check_git_tracking_for_existing_file,
    write_text_content,
)
from ..git import commit_changes
from ..line_endings import detect_line_endings
from ..mcp import mcp
from .commit_utils import append_commit_hash

# Set up logger
logger = logging.getLogger(__name__)

__all__ = [
    "edit_file",
    "find_similar_file",
    "apply_edit_pure",
]


def find_similar_file(file_path: str) -> str | None:
    """Find a similar file with a different extension.

    Args:
        file_path: The path to the file

    Returns:
        The path to a similar file, or None if none found

    """
    # Import normalize_file_path for tilde expansion
    from ..common import normalize_file_path

    # Normalize the path with tilde expansion
    file_path = normalize_file_path(file_path)

    # Simple implementation - in a real app, would check for files with different extensions
    directory = os.path.dirname(file_path)
    if not os.path.exists(directory):
        return None

    base_name = os.path.splitext(os.path.basename(file_path))[0]
    for f in os.listdir(directory):
        if f.startswith(base_name + ".") and f != os.path.basename(file_path):
            return os.path.join(directory, f)
    return None


def apply_edit_pure(
    content: str,
    old_string: str,
    new_string: str,
) -> Tuple[List[Dict[str, Any]], str, Optional[str]]:
    """Apply an edit to content using robust matching strategies.

    Args:
        content: The original content
        old_string: The text to replace
        new_string: The text to replace it with

    Returns:
        A tuple of (patch, updated_content, error_message)
        error_message is None if the edit was successful, otherwise it contains an error message

    """
    # For creating a new file, just return the new content
    if not old_string.strip():
        updated_file = new_string
        old_lines: List[str] = []
        new_lines = new_string.split("\n")

        # Create a simple patch structure
        result_patch: List[Dict[str, Any]] = [
            {
                "oldStart": 1,
                "oldLines": 0,
                "newStart": 1,
                "newLines": len(new_lines),
                "lines": [f"+{line}" for line in new_lines],
            },
        ]

        return result_patch, updated_file, None

    # First try direct replacement (most common case and efficient)
    if old_string in content:
        # Check for uniqueness of old_string before applying the replacement
        # We need to check this to avoid ambiguity in matches
        if content.count(old_string) > 1:
            # First try to use the dotdotdots approach which handles multiple matches by context
            try:
                test_result = try_dotdotdots(content, old_string, new_string)
                if test_result:
                    # If it worked with dotdotdots, we're good to proceed
                    logger.debug(
                        "Successfully used dotdotdots strategy to handle multiple occurrences",
                    )
                    # Return the result from dotdotdots
                    updated_file = test_result

                    # Create a useful diff/patch structure for dotdotdots result
                    dotdot_patch: List[Dict[str, Any]] = []
                    if (
                        content != updated_file
                    ):  # Only create a patch if there were actual changes
                        old_lines = old_string.split("\n")
                        new_lines = new_string.split("\n")

                        # Try to find the line number where the change occurs
                        try:
                            # This is a simplification; for exact matches this works,
                            # but for fuzzy matches we would need a more sophisticated approach
                            before_text = content.split(old_string)[0]
                            line_num = before_text.count("\n")
                        except Exception:
                            # Fallback: just say it's at the start of the file
                            line_num = 0

                        dotdot_patch.append(
                            {
                                "oldStart": line_num + 1,
                                "oldLines": len(old_lines),
                                "newStart": line_num + 1,
                                "newLines": len(new_lines),
                                "lines": [f"-{line}" for line in old_lines]
                                + [f"+{line}" for line in new_lines],
                            },
                        )

                    return dotdot_patch, updated_file, None
                else:
                    # Fall back to the original error message
                    matches = content.count(old_string)
                    return (
                        [],
                        content,
                        f"Found {matches} matches of the string to replace. For safety, this tool only supports replacing exactly one occurrence at a time. Add more lines of context to your edit and try again.",
                    )
            except ValueError:
                # If dotdotdots approach failed, give the original error message
                matches = content.count(old_string)
                return (
                    [],
                    content,
                    f"Found {matches} matches of the string to replace. For safety, this tool only supports replacing exactly one occurrence at a time. Add more lines of context to your edit and try again.",
                )

        # If we get here, there's only one occurrence of old_string in content
        updated_file = content.replace(old_string, new_string, 1)

        # Create a useful diff/patch structure
        diff_patch: List[Dict[str, Any]] = []
        if content != updated_file:  # Only create a patch if there were actual changes
            old_lines = old_string.split("\n")
            new_lines = new_string.split("\n")

            # Try to find the line number where the change occurs
            try:
                # This is a simplification; for exact matches this works,
                # but for fuzzy matches we would need a more sophisticated approach
                before_text = content.split(old_string)[0]
                line_num = before_text.count("\n")
            except Exception:
                # Fallback: just say it's at the start of the file
                line_num = 0

            diff_patch.append(
                {
                    "oldStart": line_num + 1,
                    "oldLines": len(old_lines),
                    "newStart": line_num + 1,
                    "newLines": len(new_lines),
                    "lines": [f"-{line}" for line in old_lines]
                    + [f"+{line}" for line in new_lines],
                },
            )

        return diff_patch, updated_file, None

    # Try with trailing whitespace stripped from each line
    content_lines = content.splitlines()
    old_lines = old_string.splitlines()

    # Check if we can find a match ignoring trailing whitespace
    content_lines_stripped = [line.rstrip() for line in content_lines]
    old_lines_stripped = [line.rstrip() for line in old_lines]

    old_text_stripped = "\n".join(old_lines_stripped)
    content_stripped = "\n".join(content_lines_stripped)

    if old_text_stripped in content_stripped:
        # Find the position in the stripped content
        start_pos = content_stripped.find(old_text_stripped)

        # Count newlines to find the line number
        line_num = content_stripped[:start_pos].count("\n")

        # Replace those lines with the new content
        result_lines = (
            content_lines[:line_num]
            + new_string.splitlines()
            + content_lines[line_num + len(old_lines) :]
        )
        updated_file = "\n".join(result_lines)
        if not content.endswith("\n") and updated_file.endswith("\n"):
            updated_file = updated_file[:-1]
        elif content.endswith("\n") and not updated_file.endswith("\n"):
            updated_file += "\n"

        # Create a useful diff/patch structure
        whitespace_patch: List[Dict[str, Any]] = []
        if content != updated_file:  # Only create a patch if there were actual changes
            # Try to find the line number where the change occurs
            whitespace_patch.append(
                {
                    "oldStart": line_num + 1,
                    "oldLines": len(old_lines),
                    "newStart": line_num + 1,
                    "newLines": len(new_string.splitlines()),
                    "lines": [f"-{line}" for line in old_lines]
                    + [f"+{line}" for line in new_string.splitlines()],
                },
            )

        return whitespace_patch, updated_file, None
    else:
        logger.debug("All matching techniques failed. No changes made.")
        return [], content, "String to replace not found in file."


async def apply_edit(
    file_path: str,
    old_string: str,
    new_string: str,
) -> Tuple[List[Dict[str, Any]], str, Optional[str]]:
    """Apply an edit to a file using robust matching strategies.

    Args:
        file_path: The path to the file
        old_string: The text to replace
        new_string: The text to replace it with

    Returns:
        A tuple of (patch, updated_file, error_message)
        error_message is None if the edit was successful, otherwise it contains an error message

    """
    # Import normalize_file_path for tilde expansion
    from ..common import normalize_file_path

    # Normalize the path with tilde expansion
    file_path = normalize_file_path(file_path)

    if os.path.exists(file_path):
        content = await async_open_text(file_path, encoding="utf-8")
    else:
        content = ""

    return apply_edit_pure(content, old_string, new_string)


def prep(content: str) -> tuple[str, list[str]]:
    """Prepare content for comparison by ensuring it ends with a newline
    and splitting into lines with preserved line endings.

    Args:
        content: Text content to prepare

    Returns:
        Tuple of (normalized content, list of lines with line endings)

    """
    if content and not content.endswith("\n"):
        content += "\n"
    lines = content.splitlines(keepends=True)
    return content, lines


def perfect_or_whitespace(
    whole_lines: list[str],
    part_lines: list[str],
    replace_lines: list[str],
) -> str | None:
    """Try perfect match first, then try with whitespace flexibility.

    Args:
        whole_lines: Original file lines with line endings
        part_lines: Lines to find/replace with line endings
        replace_lines: Replacement lines with line endings

    Returns:
        Updated content if a match was found, None otherwise

    """
    # Try for a perfect match
    res = perfect_replace(whole_lines, part_lines, replace_lines)
    if res:
        return res

    # Try being flexible about leading whitespace
    res = replace_part_with_missing_leading_whitespace(
        whole_lines,
        part_lines,
        replace_lines,
    )
    if res:
        return res

    return None


def perfect_replace(
    whole_lines: list[str],
    part_lines: list[str],
    replace_lines: list[str],
) -> str | None:
    """Find an exact match of part_lines in whole_lines and replace with replace_lines.

    Args:
        whole_lines: Original file lines with line endings
        part_lines: Lines to find/replace with line endings
        replace_lines: Replacement lines with line endings

    Returns:
        Updated content if a perfect match was found, None otherwise

    """
    part_tup = tuple(part_lines)
    part_len = len(part_lines)

    for i in range(len(whole_lines) - part_len + 1):
        whole_tup = tuple(whole_lines[i : i + part_len])
        whole_tup_stripped = tuple(
            line.rstrip() for line in whole_lines[i : i + part_len]
        )
        part_tup_stripped = tuple(line.rstrip() for line in part_lines)
        if part_tup == whole_tup or part_tup_stripped == whole_tup_stripped:
            res = whole_lines[:i] + replace_lines + whole_lines[i + part_len :]
            return "".join(res)

    return None


def match_but_for_leading_whitespace(
    whole_lines: list[str],
    part_lines: list[str],
) -> str | None:
    """Check if lines match except for consistent leading whitespace.

    Args:
        whole_lines: Original file lines subset
        part_lines: Lines to find/replace

    Returns:
        The consistent leading whitespace prefix to be added, or None if no match

    """
    num = len(whole_lines)

    # does the non-whitespace all agree?
    if not all(whole_lines[i].lstrip() == part_lines[i].lstrip() for i in range(num)):
        return None

    # are they all offset the same?
    add = set(
        whole_lines[i][: len(whole_lines[i]) - len(part_lines[i])]
        for i in range(num)
        if whole_lines[i].strip()
    )

    if len(add) != 1:
        return None

    return add.pop()


def replace_part_with_missing_leading_whitespace(
    whole_lines: list[str],
    part_lines: list[str],
    replace_lines: list[str],
) -> str | None:
    """Handle case where search text is missing the exact leading whitespace.

    Args:
        whole_lines: Original file lines with line endings
        part_lines: Lines to find/replace with line endings
        replace_lines: Replacement lines with line endings

    Returns:
        Updated content if match was found after whitespace normalization, None otherwise

    """
    # Outdent everything in part_lines and replace_lines by the max fixed amount possible
    leading = [len(p) - len(p.lstrip()) for p in part_lines if p.strip()] + [
        len(p) - len(p.lstrip()) for p in replace_lines if p.strip()
    ]

    if leading and min(leading):
        num_leading = min(leading)
        part_lines = [p[num_leading:] if p.strip() else p for p in part_lines]
        replace_lines = [p[num_leading:] if p.strip() else p for p in replace_lines]

    # can we find an exact match not including the leading whitespace
    num_part_lines = len(part_lines)

    for i in range(len(whole_lines) - num_part_lines + 1):
        add_leading = match_but_for_leading_whitespace(
            whole_lines[i : i + num_part_lines],
            part_lines,
        )

        if add_leading is None:
            continue

        replace_lines = [
            add_leading + rline if rline.strip() else rline for rline in replace_lines
        ]
        whole_lines = (
            whole_lines[:i] + replace_lines + whole_lines[i + num_part_lines :]
        )
        return "".join(whole_lines)

    return None


def try_dotdotdots(whole: str, part: str, replace: str) -> str | None:
    """Handle search/replace blocks that use ... to match code sections.

    Args:
        whole: Original file content
        part: Text to find/replace
        replace: Replacement text

    Returns:
        Updated content if dots matching was successful, None if no dots present,
        raises ValueError if dots are inconsistent

    """
    dots_re = re.compile(r"(^\s*\.\.\.\n)", re.MULTILINE | re.DOTALL)

    part_pieces = re.split(dots_re, part)
    replace_pieces = re.split(dots_re, replace)

    if len(part_pieces) != len(replace_pieces):
        raise ValueError("Unpaired ... in search/replace block")

    if len(part_pieces) == 1:
        # no dots in this edit block, just return None
        return None

    # Compare odd strings in part_pieces and replace_pieces
    all_dots_match = all(
        part_pieces[i] == replace_pieces[i] for i in range(1, len(part_pieces), 2)
    )

    if not all_dots_match:
        raise ValueError("Unmatched ... in search/replace block")

    part_pieces = [part_pieces[i] for i in range(0, len(part_pieces), 2)]
    replace_pieces = [replace_pieces[i] for i in range(0, len(replace_pieces), 2)]

    pairs = list(zip(part_pieces, replace_pieces))
    for part, replace in pairs:
        if not part and not replace:
            continue

        if not part and replace:
            if not whole.endswith("\n"):
                whole += "\n"
            whole += replace
            continue

        if whole.count(part) == 0:
            raise ValueError("Search text not found in file")
        if whole.count(part) > 1:
            raise ValueError("Multiple matches for search text - add more context")

        whole = whole.replace(part, replace, 1)

    return whole


def replace_closest_edit_distance(
    whole_lines: list[str],
    part: str,
    part_lines: list[str],
    replace_lines: list[str],
    similarity_thresh: float = 0.8,
) -> str | None:
    """Find and replace the chunk in whole_lines most similar to part_lines.

    Args:
        whole_lines: Original file lines with line endings
        part: Original search text as a single string
        part_lines: Original search text split into lines with line endings
        replace_lines: Replacement lines with line endings
        similarity_thresh: Minimum similarity threshold (0.0-1.0)

    Returns:
        Updated content if a similar enough match was found, None otherwise

    """
    max_similarity = 0
    most_similar_chunk_start = -1
    most_similar_chunk_end = -1

    scale = 0.1
    min_len = math.floor(len(part_lines) * (1 - scale))
    max_len = math.ceil(len(part_lines) * (1 + scale))

    for length in range(min_len, max_len):
        for i in range(len(whole_lines) - length + 1):
            chunk = whole_lines[i : i + length]
            chunk = "".join(chunk)

            similarity = SequenceMatcher(None, chunk, part).ratio()

            if similarity > max_similarity:
                max_similarity = similarity
                most_similar_chunk_start = i
                most_similar_chunk_end = i + length

    if max_similarity < similarity_thresh:
        return None

    modified_whole = (
        whole_lines[:most_similar_chunk_start]
        + replace_lines
        + whole_lines[most_similar_chunk_end:]
    )
    modified_whole = "".join(modified_whole)

    return modified_whole


def find_similar_lines(
    search_lines: str,
    content_lines: str,
    threshold: float = 0.6,
) -> str:
    """Find lines in content that are similar to search_lines.

    Args:
        search_lines: Text we're trying to match
        content_lines: Content of the file to search in
        threshold: Similarity threshold (0.0-1.0)

    Returns:
        String containing the most similar lines, or empty string if none found

    """
    search_lines_list = search_lines.splitlines()
    content_lines_list = content_lines.splitlines()

    # Handle empty input cases
    if not search_lines_list or not content_lines_list:
        return ""

    best_ratio = 0
    best_match: list[str] = []  # Initialize with empty list to avoid None checks
    best_match_i = 0  # Initialize to avoid unbound variable errors

    for i in range(len(content_lines_list) - len(search_lines_list) + 1):
        chunk = content_lines_list[i : i + len(search_lines_list)]
        ratio = SequenceMatcher(None, search_lines_list, chunk).ratio()
        if ratio > best_ratio:
            best_ratio = ratio
            best_match = chunk
            best_match_i = i

    if best_ratio < threshold:
        return ""

    if (
        best_match[0] == search_lines_list[0]
        and best_match[-1] == search_lines_list[-1]
    ):
        return "\n".join(best_match)

    N = 5
    best_match_end = min(
        len(content_lines_list), best_match_i + len(search_lines_list) + N
    )
    best_match_i = max(0, best_match_i - N)

    best = content_lines_list[best_match_i:best_match_end]
    return "\n".join(best)


def replace_most_similar_chunk(whole: str, part: str, replace: str) -> str | None:
    """Best efforts to find the `part` lines in `whole` and replace them with `replace`.

    Args:
        whole: Original file content
        part: Text to find/replace
        replace: Replacement text

    Returns:
        Updated content if a match was found, None otherwise

    """
    whole, whole_lines = prep(whole)
    part, part_lines = prep(part)
    replace, replace_lines = prep(replace)

    # Try perfect match or whitespace-flexible match
    res = perfect_or_whitespace(whole_lines, part_lines, replace_lines)
    if res:
        return res

    # drop leading empty line, LLMs sometimes add them spuriously
    if len(part_lines) > 2 and not part_lines[0].strip():
        skip_blank_line_part_lines = part_lines[1:]
        res = perfect_or_whitespace(
            whole_lines,
            skip_blank_line_part_lines,
            replace_lines,
        )
        if res:
            return res

    # Try to handle when it elides code with ...
    try:
        res = try_dotdotdots(whole, part, replace)
        if res:
            # If it worked with dotdotdots, we're good to proceed
            logger.debug(
                "Successfully used dotdotdots strategy to handle multiple occurrences",
            )
        else:
            # Fall back to the original error message
            matches = whole.count(part)
            raise ValueError(
                f"Found {matches} matches of the string to replace. For safety, this tool only supports replacing exactly one occurrence at a time. Add more lines of context to your edit and try again."
            )
    except ValueError:
        # If dotdotdots approach failed, give the original error message
        matches = whole.count(part)
        raise ValueError(
            f"Found {matches} matches of the string to replace. For safety, this tool only supports replacing exactly one occurrence at a time. Add more lines of context to your edit and try again."
        )

    # Try fuzzy matching
    res = replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines)
    if res:
        return res

    return None


def debug_string_comparison(
    s1: str,
    s2: str,
    label1: str = "string1",
    label2: str = "string2",
) -> bool:
    """Thoroughly debug string comparison and identify differences.

    Args:
        s1: First string
        s2: Second string
        label1: Label for the first string
        label2: Label for the second string

    Returns:
        True if strings are different, False if they are the same

    """
    # Basic checks
    length_same = len(s1) == len(s2)
    content_same = s1 == s2

    logger.debug("String comparison debug:")
    logger.debug(f"  Length same? {length_same} ({len(s1)} vs {len(s2)})")
    logger.debug(f"  Content same? {content_same}")

    # Hash check
    hash1 = hashlib.md5(s1.encode("utf-8")).hexdigest()
    hash2 = hashlib.md5(s2.encode("utf-8")).hexdigest()
    logger.debug(f"  MD5 hashes: {hash1} vs {hash2}")

    # If strings appear to be the same but should be different
    if content_same:
        # Check for invisible characters or encoding issues
        s1_repr = repr(s1)
        s2_repr = repr(s2)
        logger.debug(f"  Repr comparison: {s1_repr[:100]} vs {s2_repr[:100]}")

        # Check byte by byte
        bytes1 = s1.encode("utf-8")
        bytes2 = s2.encode("utf-8")
        if bytes1 != bytes2:
            logger.debug(
                "  Strings differ at byte level even though they appear equal as strings!",
            )

            # Find the first differing byte
            for i, (b1, b2) in enumerate(zip(bytes1, bytes2, strict=False)):
                if b1 != b2:
                    logger.debug(
                        f"  First byte difference at position {i}: {b1} vs {b2}",
                    )
                    break
    else:
        # Find differences
        diff = list(difflib.ndiff(s1.splitlines(), s2.splitlines()))
        changes = [d for d in diff if d.startswith("+ ") or d.startswith("- ")]
        if changes:
            logger.debug("  Line differences (first 5):")
            for d in changes[:5]:
                logger.debug(f"    {d}")

        # Check if strings are equal after stripping trailing whitespace
        s1_no_trailing = "\n".join([line.rstrip() for line in s1.splitlines()])
        s2_no_trailing = "\n".join([line.rstrip() for line in s2.splitlines()])
        if s1_no_trailing == s2_no_trailing:
            logger.debug(
                "  Strings match when trailing whitespace is stripped from each line!",
            )

        # Check if strings are equal after normalizing only whitespace-only lines
        s1_normalized = "\n".join(
            [line.rstrip() if line.strip() == "" else line for line in s1.splitlines()],
        )
        s2_normalized = "\n".join(
            [line.rstrip() if line.strip() == "" else line for line in s2.splitlines()],
        )
        if s1_normalized == s2_normalized:
            logger.debug("  Strings match when normalizing only whitespace-only lines!")

    return not content_same


@mcp.tool()
async def edit_file(
    path: str,
    old_string: str | None = None,
    new_string: str | None = None,
    read_file_timestamps: dict[str, float] | None = None,
    description: str | None = None,
    chat_id: str | None = None,
    commit_hash: str | None = None,
) -> str:
    """This is a tool for editing files. For larger edits, use the WriteFile tool to overwrite files.
    Provide a short description of the change.

    Before using this tool:

    1. Use the ReadFile tool to understand the file's contents and context

    2. Verify the directory path is correct (only applicable when creating new files):
       - Use the LS tool to verify the parent directory exists and is the correct location

    To make a file edit, provide the following:
    1. path: The absolute path to the file to modify (must be absolute, not relative)
    2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
    3. new_string: The edited text to replace the old_string

    The tool will replace ONE occurrence of old_string with new_string in the specified file.

    CRITICAL REQUIREMENTS FOR USING THIS TOOL:

    1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
       - Include AT LEAST 3-5 lines of context BEFORE the change point
       - Include AT LEAST 3-5 lines of context AFTER the change point
       - Include all whitespace, indentation, and surrounding code exactly as it appears in the file

    2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
       - Make separate calls to this tool for each instance
       - Each call must uniquely identify its specific instance using extensive context

    3. VERIFICATION: Before using this tool:
       - Check how many instances of the target text exist in the file
       - If multiple instances exist, gather enough context to uniquely identify each one
       - Plan separate tool calls for each instance

    WARNING: If you do not follow these requirements:
       - The tool will fail if old_string matches multiple locations
       - The tool will fail if old_string doesn't match exactly (including whitespace)
       - You may change the wrong instance if you don't include enough context

    When making edits:
       - Ensure the edit results in idiomatic, correct code
       - Do not leave the code in a broken state
       - Always use absolute file paths (starting with /)

    Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.

    Args:
        path: The absolute path to the file to edit
        old_string: The text to replace (use empty string for new file creation)
        new_string: The new text to replace old_string with
        read_file_timestamps: Dictionary mapping file paths to timestamps when they were last read
        description: Short description of the change
        chat_id: The unique ID of the current chat session
        commit_hash: Optional Git commit hash for version tracking

    Returns:
        A success message

    Note:
        This function allows creating new files when old_string is empty and the file doesn't exist.
        For existing files, it will reject attempts to edit files that are not tracked by git.
        Files must be tracked in the git repository before they can be modified.

    """
    # Set default values
    old_string = "" if old_string is None else old_string
    new_string = "" if new_string is None else new_string
    description = "" if description is None else description
    chat_id = "" if chat_id is None else chat_id

    # Normalize the file path
    full_file_path = normalize_file_path(path)

    # Normalize string inputs to ensure consistent newlines
    old_string = old_string.replace("\r\n", "\n")
    new_string = new_string.replace("\r\n", "\n")

    # Prevent editing codemcp.toml for security reasons
    if os.path.basename(full_file_path) == "codemcp.toml":
        raise ValueError("Editing codemcp.toml is not allowed for security reasons.")

    # Check file path and permissions
    is_valid, error_message = await check_file_path_and_permissions(full_file_path)
    if not is_valid:
        raise ValueError(error_message)

    # Handle creating a new file - skip commit_pending_changes for non-existent files
    creating_new_file = old_string == "" and not os.path.exists(full_file_path)

    if not creating_new_file:
        # Only check commit_pending_changes for existing files
        is_tracked, track_error = await check_git_tracking_for_existing_file(
            full_file_path,
            chat_id=chat_id,
        )
        if not is_tracked:
            raise ValueError(track_error)

    # Debug string comparison using our thorough utility
    strings_are_different = debug_string_comparison(
        old_string,
        new_string,
        "old_string",
        "new_string",
    )

    if not strings_are_different:
        return "No changes to make: old_string and new_string are exactly the same."

    # Proceed with the edit now that we've confirmed the strings are different

    # Handle creating a new file
    if old_string == "" and os.path.exists(full_file_path):
        raise FileExistsError("Cannot create new file - file already exists.")

    # Handle creating a new file
    if old_string == "" and not os.path.exists(full_file_path):
        directory = os.path.dirname(full_file_path)
        os.makedirs(directory, exist_ok=True)
        await write_text_content(full_file_path, new_string)

        # Commit the changes
        success, message = await commit_changes(full_file_path, description, chat_id)
        git_message = ""
        if success:
            git_message = f"\nChanges committed to git: {description}"
            # Include any extra details like previous commit hash if present in the message
            if "previous commit was" in message:
                git_message = f"\n{message}"
        else:
            git_message = f"\nFailed to commit changes to git: {message}"

        result = f"Successfully created {full_file_path}{git_message}"
        # Append commit hash
        result, _ = await append_commit_hash(result, full_file_path, commit_hash)
        return result

    # Check if file exists
    if not os.path.exists(full_file_path):
        # Try to find a similar file
        similar_file = find_similar_file(full_file_path)
        message = f"File does not exist: {full_file_path}"
        if similar_file:
            message += f" Did you mean {similar_file}?"
        raise FileNotFoundError(message)

    # Check if file is a Jupyter notebook
    if full_file_path.endswith(".ipynb"):
        raise ValueError(
            "File is a Jupyter Notebook. Use the NotebookEditTool to edit this file."
        )

    # Check if file has been read
    if read_file_timestamps and full_file_path not in read_file_timestamps:
        raise ValueError(
            "File has not been read yet. Read it first before writing to it."
        )

    # Check if file has been modified since read
    if read_file_timestamps and os.path.exists(full_file_path):
        last_write_time = os.stat(full_file_path).st_mtime
        if last_write_time > read_file_timestamps.get(full_file_path, 0):
            raise ValueError(
                "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it."
            )

    # Use UTF-8 encoding and detect line endings
    line_endings = await detect_line_endings(full_file_path, return_format="format")

    # Read the original file
    content = await async_open_text(full_file_path, encoding="utf-8")

    # Apply the edit with advanced matching if needed
    _, updated_file, error = await apply_edit(full_file_path, old_string, new_string)

    # If there was an error during apply_edit, raise it
    if error:
        raise ValueError(error)

    # If no changes were made (which should never happen at this point),
    # log a warning but continue
    if content == updated_file and old_string.strip():
        logger.warning(
            "No changes were made despite passing all checks. This is unexpected.",
        )

    # Create directory if it doesn't exist
    directory = os.path.dirname(full_file_path)
    os.makedirs(directory, exist_ok=True)

    # Write the modified content back to the file
    await write_text_content(full_file_path, updated_file, "utf-8", line_endings)

    # Update read timestamp
    if read_file_timestamps is not None:
        read_file_timestamps[full_file_path] = os.stat(full_file_path).st_mtime

    # Try to run the formatter on the file
    format_message = ""
    formatter_success, formatter_output = await run_formatter_without_commit(
        full_file_path
    )
    if formatter_success:
        logger.info(f"Auto-formatted {full_file_path}")
        if formatter_output.strip():
            format_message = "\nAuto-formatted the file"
    else:
        # Only log warning if there was actually a format command configured but it failed
        if not "No format command configured" in formatter_output:
            logger.warning(
                f"Failed to auto-format {full_file_path}: {formatter_output}"
            )

    # Generate a snippet of the edited file to show in the response
    snippet = get_edit_snippet(content, old_string, new_string)

    # Commit the changes
    git_message = ""
    success, message = await commit_changes(full_file_path, description, chat_id)
    if success:
        git_message = f"\n\nChanges committed to git: {description}"
        # Include any extra details like previous commit hash if present in the message
        if "previous commit was" in message:
            git_message = f"\n\n{message}"
    else:
        git_message = f"\n\nFailed to commit changes to git: {message}"

    result = f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{format_message}{git_message}"

    # Append commit hash
    result, _ = await append_commit_hash(result, full_file_path, commit_hash)
    return result
