import os

from aider.tools.utils.base_tool import BaseTool
from aider.tools.utils.helpers import (
    ToolError,
    apply_change,
    generate_unified_diff_snippet,
    handle_tool_error,
    validate_file_for_edit,
)


class Tool(BaseTool):
    NORM_NAME = "extractlines"
    SCHEMA = {
        "type": "function",
        "function": {
            "name": "ExtractLines",
            "description": "Extract lines from a source file and append them to a target file.",
            "parameters": {
                "type": "object",
                "properties": {
                    "source_file_path": {"type": "string"},
                    "target_file_path": {"type": "string"},
                    "start_pattern": {"type": "string"},
                    "end_pattern": {"type": "string"},
                    "line_count": {"type": "integer"},
                    "near_context": {"type": "string"},
                    "occurrence": {"type": "integer", "default": 1},
                    "dry_run": {"type": "boolean", "default": False},
                },
                "required": ["source_file_path", "target_file_path", "start_pattern"],
            },
        },
    }

    @classmethod
    def execute(
        cls,
        coder,
        source_file_path,
        target_file_path,
        start_pattern,
        end_pattern=None,
        line_count=None,
        near_context=None,
        occurrence=1,
        dry_run=False,
    ):
        """
        Extract a range of lines from a source file and move them to a target file.

        Parameters:
        - coder: The Coder instance
        - source_file_path: Path to the file to extract lines from
        - target_file_path: Path to the file to append extracted lines to (will be created if needed)
        - start_pattern: Pattern marking the start of the block to extract
        - end_pattern: Optional pattern marking the end of the block
        - line_count: Optional number of lines to extract (alternative to end_pattern)
        - near_context: Optional text nearby to help locate the correct instance of the start_pattern
        - occurrence: Which occurrence of the start_pattern to use (1-based index, or -1 for last)
        - dry_run: If True, simulate the change without modifying files

        Returns a result message.
        """
        tool_name = "ExtractLines"
        try:
            # --- Validate Source File ---
            abs_source_path, rel_source_path, source_content = validate_file_for_edit(
                coder, source_file_path
            )

            # --- Validate Target File ---
            abs_target_path = coder.abs_root_path(target_file_path)
            rel_target_path = coder.get_rel_fname(abs_target_path)
            target_exists = os.path.isfile(abs_target_path)

            if target_exists:
                # If target exists, validate it for editing
                try:
                    _, _, target_content = validate_file_for_edit(coder, target_file_path)
                except ToolError as e:
                    coder.io.tool_error(f"Target file validation failed: {str(e)}")
                    return f"Error: {str(e)}"
            else:
                # Target doesn't exist, start with empty content
                target_content = ""

            # --- Find Extraction Range ---
            if end_pattern and line_count:
                coder.io.tool_error("Cannot specify both end_pattern and line_count")
                return "Error: Cannot specify both end_pattern and line_count"

            source_lines = source_content.splitlines()

            start_pattern_line_indices = []
            for i, line in enumerate(source_lines):
                if start_pattern in line:
                    if near_context:
                        context_window_start = max(0, i - 5)
                        context_window_end = min(len(source_lines), i + 6)
                        context_block = "\n".join(
                            source_lines[context_window_start:context_window_end]
                        )
                        if near_context in context_block:
                            start_pattern_line_indices.append(i)
                    else:
                        start_pattern_line_indices.append(i)

            if not start_pattern_line_indices:
                err_msg = f"Start pattern '{start_pattern}' not found"
                if near_context:
                    err_msg += f" near context '{near_context}'"
                err_msg += f" in source file '{source_file_path}'."
                coder.io.tool_error(err_msg)
                return f"Error: {err_msg}"

            num_occurrences = len(start_pattern_line_indices)
            try:
                occurrence = int(occurrence)
                if occurrence == -1:
                    target_idx = num_occurrences - 1
                elif occurrence > 0 and occurrence <= num_occurrences:
                    target_idx = occurrence - 1
                else:
                    err_msg = (
                        f"Occurrence number {occurrence} is out of range for start pattern"
                        f" '{start_pattern}'. Found {num_occurrences} occurrences"
                    )
                    if near_context:
                        err_msg += f" near '{near_context}'"
                    err_msg += f" in '{source_file_path}'."
                    coder.io.tool_error(err_msg)
                    return f"Error: {err_msg}"
            except ValueError:
                coder.io.tool_error(
                    f"Invalid occurrence value: '{occurrence}'. Must be an integer."
                )
                return f"Error: Invalid occurrence value '{occurrence}'"

            start_line = start_pattern_line_indices[target_idx]
            occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""

            end_line = -1
            if end_pattern:
                for i in range(start_line, len(source_lines)):
                    if end_pattern in source_lines[i]:
                        end_line = i
                        break
                if end_line == -1:
                    err_msg = (
                        f"End pattern '{end_pattern}' not found after {occurrence_str}start"
                        f" pattern '{start_pattern}' (line {start_line + 1}) in"
                        f" '{source_file_path}'."
                    )
                    coder.io.tool_error(err_msg)
                    return f"Error: {err_msg}"
            elif line_count:
                try:
                    line_count = int(line_count)
                    if line_count <= 0:
                        raise ValueError("Line count must be positive")
                    end_line = min(start_line + line_count - 1, len(source_lines) - 1)
                except ValueError:
                    coder.io.tool_error(
                        f"Invalid line_count value: '{line_count}'. Must be a positive integer."
                    )
                    return f"Error: Invalid line_count value '{line_count}'"
            else:
                end_line = start_line  # Extract just the start line if no end specified

            # --- Prepare Content Changes ---
            extracted_lines = source_lines[start_line : end_line + 1]
            new_source_lines = source_lines[:start_line] + source_lines[end_line + 1 :]
            new_source_content = "\n".join(new_source_lines)

            # Append extracted lines to target content, ensuring a newline if target wasn't empty
            extracted_block = "\n".join(extracted_lines)
            if target_content and not target_content.endswith("\n"):
                target_content += "\n"  # Add newline before appending if needed
            new_target_content = target_content + extracted_block

            # --- Generate Diffs ---
            source_diff_snippet = generate_unified_diff_snippet(
                source_content, new_source_content, rel_source_path
            )
            target_insertion_line = len(target_content.splitlines()) if target_content else 0
            target_diff_snippet = generate_unified_diff_snippet(
                target_content, new_target_content, rel_target_path
            )

            # --- Handle Dry Run ---
            if dry_run:
                num_extracted = end_line - start_line + 1
                target_action = "append to" if target_exists else "create"
                coder.io.tool_output(
                    f"Dry run: Would extract {num_extracted} lines (from {occurrence_str}start"
                    f" pattern '{start_pattern}') in {source_file_path} and {target_action}"
                    f" {target_file_path}"
                )
                # Provide more informative dry run response with diffs
                return (
                    f"Dry run: Would extract {num_extracted} lines from {rel_source_path} and"
                    f" {target_action} {rel_target_path}.\nSource Diff"
                    f" (Deletion):\n{source_diff_snippet}\nTarget Diff"
                    f" (Insertion):\n{target_diff_snippet}"
                )

            # --- Apply Changes (Not Dry Run) ---
            # Apply source change
            source_metadata = {
                "start_line": start_line + 1,
                "end_line": end_line + 1,
                "start_pattern": start_pattern,
                "end_pattern": end_pattern,
                "line_count": line_count,
                "near_context": near_context,
                "occurrence": occurrence,
                "extracted_content": extracted_block,
                "target_file": rel_target_path,
            }
            source_change_id = apply_change(
                coder,
                abs_source_path,
                rel_source_path,
                source_content,
                new_source_content,
                "extractlines_source",
                source_metadata,
            )

            # Apply target change
            target_metadata = {
                "insertion_line": target_insertion_line + 1,
                "inserted_content": extracted_block,
                "source_file": rel_source_path,
            }
            target_change_id = apply_change(
                coder,
                abs_target_path,
                rel_target_path,
                target_content,
                new_target_content,
                "extractlines_target",
                target_metadata,
            )

            # --- Update Context ---
            coder.files_edited_by_tools.add(rel_source_path)
            coder.files_edited_by_tools.add(rel_target_path)

            if not target_exists:
                # Add the newly created file to editable context
                coder.abs_fnames.add(abs_target_path)
                coder.io.tool_output(
                    f"✨ Created and added '{target_file_path}' to editable context."
                )

            # --- Return Result ---
            num_extracted = end_line - start_line + 1
            target_action = "appended to" if target_exists else "created"
            coder.io.tool_output(
                f"✅ Extracted {num_extracted} lines from {rel_source_path} (change_id:"
                f" {source_change_id}) and {target_action} {rel_target_path} (change_id:"
                f" {target_change_id})"
            )
            # Provide more informative success response with change IDs and diffs
            return (
                f"Successfully extracted {num_extracted} lines from {rel_source_path} and"
                f" {target_action} {rel_target_path}.\nSource Change ID:"
                f" {source_change_id}\nSource Diff (Deletion):\n{source_diff_snippet}\nTarget"
                f" Change ID: {target_change_id}\nTarget Diff"
                f" (Insertion):\n{target_diff_snippet}"
            )

        except ToolError as e:
            # Handle errors raised by utility functions or explicitly raised here
            return handle_tool_error(coder, tool_name, e, add_traceback=False)
        except Exception as e:
            # Handle unexpected errors
            return handle_tool_error(coder, tool_name, e)
