#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
FindText Command - Advanced text search in files
"""

import os
import re
import fnmatch
import colorama
import sys
from pathlib import Path
from typing import List, Dict, Any, Union, Optional
from Core.command_base import CommandBase

# Initialize colorama for cross-platform colored terminal output
colorama.init(autoreset=True)

class FindTextCommand(CommandBase):
    """
    Command to search for text patterns in files with advanced options
    """
    
    name = "findText"
    description = "Searches for text patterns in files with advanced filtering options"
    category = "file"
    
    parameters = [
        {
            'name': 'pattern',
            'description': 'Text pattern to search for (supports regular expressions)',
            'required': True
        },
        {
            'name': 'target',
            'description': 'File or directory (or space-separated list of files/directories) to search in',
            'required': True
        },
        {
            'name': 'recursive',
            'description': 'Recursion level: -r/--recursive for unlimited depth, -rN/--recursiveN for N levels deep',
            'required': False,
            'default': None
        },
        {
            'name': 'regex',
            'description': 'Use regular expressions for pattern matching (true/false)',
            'required': False,
            'default': False
        },
        {
            'name': 'case_sensitive',
            'description': 'Perform case-sensitive search (true/false)',
            'required': False,
            'default': True
        },
        {
            'name': 'file_pattern',
            'description': 'Only search in files matching this pattern (e.g., "*.py" or "*.{py,js}")',
            'required': False,
            'default': "*"
        },
        {
            'name': 'context_lines',
            'description': 'Number of lines to show before and after each match',
            'required': False,
            'default': 0
        },
        {
            'name': 'invert_match',
            'description': 'Show lines that do NOT match the pattern (true/false)',
            'required': False,
            'default': False
        },
        {
            'name': 'count_only',
            'description': 'Only show count of matches per file (true/false)',
            'required': False,
            'default': False
        },
        {
            'name': 'max_matches',
            'description': 'Maximum number of matches to display (0 for unlimited)',
            'required': False,
            'default': 0
        },
        {
            'name': 'colored',
            'description': 'Highlight matches with color (true/false)',
            'required': False,
            'default': True
        }
    ]
    
    examples = [
        {
            'command': 'qzx findText "function" "script.js"',
            'description': 'Find all occurrences of "function" in a JavaScript file'
        },
        {
            'command': 'qzx findText "error" "logs" true',
            'description': 'Find all occurrences of "error" in all files under logs directory'
        },
        {
            'command': 'qzx findText "def\\s+\\w+" "src" true true',
            'description': 'Find all function definitions in Python files using regex'
        },
        {
            'command': 'qzx findText "WARNING|ERROR" "logs" true true false "*.log"',
            'description': 'Find warnings or errors in log files'
        },
        {
            'command': 'qzx findText "TODO" "src" true false true "*.{py,js,ts}" 2',
            'description': 'Find TODOs in source files with 2 lines of context'
        },
        {
            'command': 'qzx findText "function" "file1.js file2.js lib/utils.js"',
            'description': 'Find all occurrences of "function" in multiple specific files'
        }
    ]
    
    def _parse_recursive_parameter(self, recursive_param):
        """
        Parse the recursive parameter into a depth value
        
        Args:
            recursive_param: The raw recursive parameter (string)
            
        Returns:
            int or None: Maximum recursion depth (None for unlimited, 0 for none)
        """
        # Default is no recursion (0) if parameter is None
        if recursive_param is None:
            return 0
            
        # Only accept string parameters with the new format
        if isinstance(recursive_param, str):
            # New format: -r or --recursive for unlimited recursion
            if recursive_param in ('-r', '--recursive'):
                return None
                
            # New format: -rN or --recursiveN for N levels of recursion
            r_match = re.match(r'^-r(\d+)$', recursive_param)
            if r_match:
                return int(r_match.group(1))
                
            recursive_match = re.match(r'^--recursive(\d+)$', recursive_param)
            if recursive_match:
                return int(recursive_match.group(1))
                
        # Default to no recursion if the format is not recognized
        return 0
    
    def execute(self, pattern, target, recursive=None, regex=False, case_sensitive=True, 
                file_pattern="*", context_lines=0, invert_match=False, count_only=False, 
                max_matches=0, colored=True):
        """
        Searches for text patterns in files with advanced options
        
        Args:
            pattern (str): Text pattern to search for
            target (str): File or directory to search in (space-separated for multiple)
            recursive: Recursion level: none by default, -r/--recursive for unlimited, -rN/--recursiveN for N levels
            regex (bool): Whether to use regular expressions for pattern matching
            case_sensitive (bool): Whether the search should be case-sensitive
            file_pattern (str): Only search in files matching this pattern (e.g., "*.py")
            context_lines (int): Number of lines to show before and after each match
            invert_match (bool): Show lines that do NOT match the pattern
            count_only (bool): Only show count of matches per file
            max_matches (int): Maximum number of matches to display (0 for unlimited)
            colored (bool): Whether to colorize the output
            
        Returns:
            Dictionary with search results
        """
        try:
            # Parse recursion parameter
            recursion_depth = self._parse_recursive_parameter(recursive)
            
            # Convert other parameters to proper types
            if isinstance(regex, str):
                regex = regex.lower() in ('true', 'yes', 'y', '1')
            
            if isinstance(case_sensitive, str):
                case_sensitive = case_sensitive.lower() in ('true', 'yes', 'y', '1')
            
            if isinstance(context_lines, str):
                try:
                    context_lines = int(context_lines)
                except ValueError:
                    return {"success": False, "error": f"Invalid context_lines value: {context_lines}. Must be a number."}
            
            if isinstance(invert_match, str):
                invert_match = invert_match.lower() in ('true', 'yes', 'y', '1')
            
            if isinstance(count_only, str):
                count_only = count_only.lower() in ('true', 'yes', 'y', '1')
            
            if isinstance(max_matches, str):
                try:
                    max_matches = int(max_matches)
                except ValueError:
                    return {"success": False, "error": f"Invalid max_matches value: {max_matches}. Must be a number."}
            
            if isinstance(colored, str):
                colored = colored.lower() in ('true', 'yes', 'y', '1')
                
            # Disable colored output on Windows if there are encoding issues
            if colored and sys.platform == 'win32' and sys.stdout.encoding != 'utf-8':
                try:
                    # Try a simple colored output
                    test_colored_string = f"{colorama.Fore.RED}Test{colorama.Style.RESET_ALL}"
                    print(test_colored_string, end='')
                    # If no error, clear the line and continue
                    print('\r' + ' ' * len("Test") + '\r', end='')
                except UnicodeEncodeError:
                    # If we got an encoding error, disable colored output
                    colored = False
                    print("Warning: Colored output disabled due to terminal encoding limitations")
                    
            # Split target into individual paths
            target_paths = target.split()
            
            # Prepare regex pattern if needed
            if regex:
                try:
                    flags = 0 if case_sensitive else re.IGNORECASE
                    search_pattern = re.compile(pattern, flags)
                except re.error as e:
                    return {"success": False, "error": f"Invalid regular expression: {str(e)}"}
            else:
                search_pattern = pattern
            
            # Track results
            all_results = []
            total_matches = 0
            files_with_matches = 0
            files_searched = 0
            
            # Get a descriptive recursion string for the result message
            recursion_message = ""
            if recursion_depth is None:
                recursion_message = " (including all subdirectories)"
            elif recursion_depth > 0:
                recursion_message = f" (including subdirectories up to {recursion_depth} level{'s' if recursion_depth > 1 else ''})"
            
            # Process each target
            for path in target_paths:
                if os.path.isfile(path):
                    # Process a single file
                    files_searched += 1
                    file_matches = self._search_file(path, search_pattern, regex, case_sensitive, 
                                                  context_lines, invert_match, count_only, colored)
                    if file_matches:
                        all_results.append(file_matches)
                        total_matches += file_matches["matches"]
                        files_with_matches += 1
                        
                        # Stop if we've reached the max matches limit
                        if max_matches > 0 and total_matches >= max_matches:
                            break
                elif os.path.isdir(path):
                    # Process a directory based on recursion depth
                    for root, dirs, files in os.walk(path):
                        # Calculate current depth relative to target
                        rel_path = os.path.relpath(root, path)
                        current_depth = 0 if rel_path == '.' else rel_path.count(os.sep) + 1
                        
                        # Skip if depth exceeds the limit and not doing unlimited recursion
                        if recursion_depth is not None and current_depth > recursion_depth:
                            continue
                        
                        # Skip further processing if not recursive and not at top level
                        if recursion_depth == 0 and current_depth > 0:
                            break
                            
                        for file in files:
                            if fnmatch.fnmatch(file, file_pattern):
                                file_path = os.path.join(root, file)
                                
                                files_searched += 1
                                file_matches = self._search_file(file_path, search_pattern, regex, case_sensitive, 
                                                              context_lines, invert_match, count_only, colored)
                                if file_matches:
                                    all_results.append(file_matches)
                                    total_matches += file_matches["matches"]
                                    files_with_matches += 1
                                    
                                    # Stop if we've reached the max matches limit
                                    if max_matches > 0 and total_matches >= max_matches:
                                        break
                        
                        # Stop processing if we've reached the max matches limit
                        if max_matches > 0 and total_matches >= max_matches:
                            break
            
            # Get a descriptive recursion string for the result
            recursion_info = "unlimited"
            if recursion_depth == 0:
                recursion_info = "none"
            elif recursion_depth is not None:
                recursion_info = str(recursion_depth)
            
            # Return results
            return {
                "success": True,
                "pattern": pattern,
                "target": target,
                "recursive": recursion_info,
                "regex": regex,
                "case_sensitive": case_sensitive,
                "file_pattern": file_pattern,
                "files_searched": files_searched,
                "files_with_matches": files_with_matches,
                "total_matches": total_matches,
                "results": all_results
            }
                
        except Exception as e:
            return {
                "success": False,
                "error": f"Error searching text: {str(e)}"
            } 
    
    def _search_file(self, file_path, pattern, regex, case_sensitive, context_lines, invert_match, count_only, colored):
        """
        Search for patterns in a single file
        
        Args:
            file_path (str): Path to the file to search in
            pattern: Pattern to search for (string or compiled regex)
            regex (bool): Whether pattern is a regex
            case_sensitive (bool): Whether the search is case sensitive
            context_lines (int): Number of lines to show before and after match
            invert_match (bool): Show lines that don't match the pattern
            count_only (bool): Only count matches, don't return content
            colored (bool): Whether to colorize output
            
        Returns:
            dict: Match results for the file or None if no matches
        """
        try:
            # Read file content
            try:
                with open(file_path, 'r', encoding='utf-8', errors='replace') as file:
                    lines = file.readlines()
            except UnicodeDecodeError:
                # If UTF-8 fails, try the system's default encoding as fallback
                with open(file_path, 'r', encoding=sys.getdefaultencoding(), errors='replace') as file:
                    lines = file.readlines()
            except Exception as e:
                return {
                    "file": file_path,
                    "error": f"Could not read file: {str(e)}",
                    "matches": 0
                }
            
            # Find matches
            matches = []
            for i, line in enumerate(lines):
                if regex:
                    match = pattern.search(line)
                else:
                    # For non-regex searches
                    if case_sensitive:
                        match = pattern in line
                    else:
                        match = pattern.lower() in line.lower()
                
                if (match and not invert_match) or (not match and invert_match):
                    matches.append((i, line))
            
            if not matches:
                return None
                
            # Prepare colors for output
            if colored:
                try:
                    highlight_color = colorama.Fore.RED + colorama.Style.BRIGHT
                    line_num_color = colorama.Fore.GREEN
                    reset_color = colorama.Style.RESET_ALL
                except:
                    # If there's any issue with colorama, disable colors
                    highlight_color = ""
                    line_num_color = ""
                    reset_color = ""
            else:
                highlight_color = ""
                line_num_color = ""
                reset_color = ""
            
            match_count = len(matches)
            
            # If only counting, return count data
            if count_only:
                return {
                    "file": file_path,
                    "matches": match_count,
                    "count_only": True
                }
            
            # Format matches with context
            match_lines = []
            
            for i, (line_num, line_text) in enumerate(matches):
                # Add separator between match groups
                if i > 0 and context_lines > 0:
                    match_lines.append("---")
                
                # Add context before
                context_start = max(0, line_num - context_lines)
                for ctx_line_num in range(context_start, line_num):
                    # Use safe strings for content to prevent encoding issues
                    content = self._ensure_safe_string(lines[ctx_line_num].rstrip())
                    match_lines.append({
                        "line_num": ctx_line_num + 1,
                        "content": content,
                        "is_match": False
                    })
                
                # Add the matching line
                try:
                    if colored and not invert_match and regex:
                        # Highlight the matching part for regex patterns
                        highlighted_line = pattern.sub(
                            lambda m: f"{highlight_color}{m.group(0)}{reset_color}", 
                            line_text.rstrip()
                        )
                        match_lines.append({
                            "line_num": line_num + 1,
                            "content": self._ensure_safe_string(highlighted_line),
                            "is_match": True
                        })
                    elif colored and not invert_match and not regex:
                        # Highlight for non-regex patterns
                        if case_sensitive:
                            idx = line_text.find(pattern)
                            if idx >= 0:
                                before = line_text[:idx]
                                matched = line_text[idx:idx+len(pattern)]
                                after = line_text[idx+len(pattern):]
                                highlighted_line = f"{before}{highlight_color}{matched}{reset_color}{after}".rstrip()
                            else:
                                highlighted_line = line_text.rstrip()
                        else:
                            idx = line_text.lower().find(pattern.lower())
                            if idx >= 0:
                                before = line_text[:idx]
                                matched = line_text[idx:idx+len(pattern)]
                                after = line_text[idx+len(pattern):]
                                highlighted_line = f"{before}{highlight_color}{matched}{reset_color}{after}".rstrip()
                            else:
                                highlighted_line = line_text.rstrip()
                                
                        match_lines.append({
                            "line_num": line_num + 1,
                            "content": self._ensure_safe_string(highlighted_line),
                            "is_match": True
                        })
                    else:
                        match_lines.append({
                            "line_num": line_num + 1,
                            "content": self._ensure_safe_string(line_text.rstrip()),
                            "is_match": True
                        })
                except Exception as e:
                    # If highlighting fails, use plain version
                    match_lines.append({
                        "line_num": line_num + 1,
                        "content": self._ensure_safe_string(line_text.rstrip()),
                        "is_match": True,
                        "highlight_error": str(e)
                    })
                
                # Add context after
                context_end = min(len(lines), line_num + context_lines + 1)
                for ctx_line_num in range(line_num + 1, context_end):
                    # Use safe strings for content
                    content = self._ensure_safe_string(lines[ctx_line_num].rstrip())
                    match_lines.append({
                        "line_num": ctx_line_num + 1,
                        "content": content,
                        "is_match": False
                    })
            
            return {
                "file": file_path,
                "matches": match_count,
                "lines": match_lines
            }
            
        except Exception as e:
            return {
                "file": file_path,
                "error": str(e),
                "matches": 0
            }
            
    def _ensure_safe_string(self, text):
        """
        Ensure that a string is safe for output in the current environment
        
        Args:
            text (str): The text to make safe
            
        Returns:
            str: A safely encoded/decoded string
        """
        if text is None:
            return ""
            
        try:
            # Try to encode with the current stdout encoding, replace problematic chars
            if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding:
                return text.encode(sys.stdout.encoding, errors='replace').decode(sys.stdout.encoding)
            # Default fallback
            return text.encode('ascii', errors='replace').decode('ascii')
        except Exception:
            # Last resort: remove all non-ASCII chars
            return ''.join(c if ord(c) < 128 else '?' for c in text) 