import json
import os
import stat
import requests
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, List, Tuple, Any

MODELS = {
    'gpt-3.5-turbo': {'context': 16385, 'endpoint': 'gpt-3.5-turbo'},
    'gpt-4-turbo': {'context': 128000, 'endpoint': 'gpt-4-turbo'},
    'gpt-4o': {'context': 128000, 'endpoint': 'gpt-4o'},
    'gpt-5': {'context': 256000, 'endpoint': 'gpt-5'},
    'gpt-5-nano': {'context': 32000, 'endpoint': 'gpt-5-nano'},
}

DEFAULT_MODEL = 'gpt-5'
DEFAULT_DAILY_LIMIT = 220000

OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions'
BRAVE_SEARCH_URL = 'https://api.search.brave.com/res/v1/web/search'

CONTEXT_LIMITS = {
    'standard': 3000,
    'think': 5000,
    'think2': 10000,
    'think3': 25000,
}

INCLUDECPP_CONTEXT = '''
CRITICAL KNOWLEDGE FOR INCLUDECPP:

1. NAMESPACE REQUIREMENT:
   ALL C++ code MUST be inside `namespace includecpp { }` - this is REQUIRED, not optional.

2. PLUGIN FILE (.cp) FORMAT:
   SOURCE(file.cpp) module_name
   PUBLIC(
       module_name CLASS(MyClass) {
           CONSTRUCTOR()
           CONSTRUCTOR(int, double)
           METHOD(foo)
           METHOD_CONST(bar, const std::string&)
           FIELD(x)
       }
       module_name FUNC(standalone_function)
       module_name TEMPLATE_FUNC(generic_func) TYPES(int, float, double)
       module_name STRUCT(Point) { FIELD(x) FIELD(y) }
   )

3. BUILD OUTPUT:
   ~/.includecpp/builds/ (Windows: %APPDATA%/IncludeCPP/)

4. COMMON ERRORS AND FIXES:
   * "undefined reference" -> Code not in namespace includecpp { }, or missing FUNC() in .cp
   * "no matching function" -> Wrong parameter types in .cp METHOD() definition
   * "template instantiation" -> Missing TEMPLATE_FUNC() with TYPES() in .cp file
   * "namespace includecpp not found" -> Source file missing namespace wrapper
   * "no member named X" -> Method not in class public section, or missing METHOD() in .cp

5. FIELD DECLARATIONS:
   FIELD(name) - only the field name is needed, not the type.
   Comma-separated fields like `double x, y, z;` are parsed as separate fields.
'''

SYSTEM_PROMPT_OPTIMIZE = '''You are a C++ expert specializing in pybind11 bindings and the IncludeCPP framework.

IMPORTANT: Be conservative. Only suggest safe optimizations that will NOT break compilation.

Rules:
1. NEVER remove existing functions or code blocks
2. Preserve all public API signatures EXACTLY
3. NEVER change function parameters or return types
4. Only make changes that are 100% safe and backwards compatible
5. Maintain namespace includecpp structure
6. Do not add comments, docstrings, or explanatory text
7. Do not add AI-typical output markers or annotations
8. If unsure about a change, DO NOT make it

Safe optimizations:
- Adding const where appropriate
- Using reserve() before loops
- Replacing raw loops with STL algorithms (if equivalent)
- Adding noexcept to functions that don't throw
- Using move semantics for local variables

AVOID:
- Changing function signatures
- Removing code that might be used elsewhere
- Complex refactoring
- Template changes

If a change would alter existing functionality, you MUST prefix with:
CONFIRM_REQUIRED: <brief description>

Output format for each file:
FILE: <relative_path>
CHANGES:
- Line X: <what changed> - <why>
- Line Y-Z: <what changed> - <why>
```cpp
<complete file content with no comments about changes>
```

Only output files that need changes. If no safe optimizations found, respond with: NO_CHANGES_NEEDED'''

SYSTEM_PROMPT_FIX = '''You are a C++ expert analyzing code for the IncludeCPP framework.

Context:
- IncludeCPP generates pybind11 bindings from .cp plugin files
- SOURCE() and HEADER() directives link to C++ source files
- All exposed code MUST be inside namespace includecpp { ... }
- Modules compile to .pyd (Windows) or .so (Linux/Mac) Python extensions

CRITICAL CHECKS (in order):
1. SYNTAX ERRORS: Missing semicolons, unmatched braces, invalid tokens
2. NAMESPACE: All classes/functions MUST be inside namespace includecpp { }
3. TYPE ERRORS: Undefined types, wrong return types, parameter mismatches
4. MEMORY: Dangling pointers, leaks, uninitialized variables
5. PYBIND11: Return policies, holder types, opaque types

Rules:
1. Never remove functions or change public API signatures
2. Fix ALL syntax errors first - code must compile
3. Ensure code is inside namespace includecpp { }
4. Do not add comments or docstrings
5. Preserve exact logic and behavior

If removal is necessary, prefix with:
CONFIRM_REQUIRED: <description>

Output format for each file:
FILE: <path>
CHANGES:
- Line X: <what changed> - <why>
- Line Y-Z: <what changed> - <why>
```cpp
<complete fixed file content>
```

Only output files that need changes. If no issues found, respond with: NO_ISSUES_FOUND'''

SYSTEM_PROMPT_BUILD_ERROR = '''You are analyzing a C++ build error in an IncludeCPP project.

IncludeCPP Architecture:
- Plugin files (.cp) define bindings: SOURCE(file.cpp), HEADER(file.h), CLASS(), METHOD(), FUNC()
- SOURCE() links C++ implementation files
- HEADER() links header files for declarations
- All exposed code MUST be in namespace includecpp { }
- Build generates pybind11 bindings in a temp bindings.cpp file
- Compiles to .pyd (Windows) or .so (Linux/Mac) Python extensions

Common Error Categories:
1. USER CODE ERROR: Syntax errors, missing includes, type mismatches in user C++ files
2. PLUGIN FILE ERROR: Wrong signatures in .cp file, missing METHOD(), wrong parameter types
3. NAMESPACE ERROR: Code not inside namespace includecpp { }
4. BINDING ERROR: pybind11 type conversion issues, missing opaque declarations
5. INCLUDECPP BUG: Framework generated incorrect bindings (rare, report at github.com/liliassg/IncludeCPP/issues)

Analyze the error and provide:

ERROR TYPE: [USER CODE | PLUGIN FILE | NAMESPACE | BINDING | INCLUDECPP BUG]

ROOT CAUSE:
<one line explanation>

FIX:
File: <exact file path>
Line: <line number if known>
```cpp
<exact code fix>
```

WHY:
<brief explanation of what was wrong>

PREVENTION:
<one line tip to avoid this in future>'''

SYSTEM_PROMPT_AGENT = '''You are a C++ expert working on an IncludeCPP project.

Context:
- IncludeCPP generates pybind11 bindings
- Code must be in namespace includecpp
- Focus on the specific task requested

Rules:
1. Never remove existing functions unless explicitly asked
2. Preserve public API signatures
3. No comments, docstrings, or explanatory markers
4. Output complete file contents

If removal needed, prefix with:
CONFIRM_REQUIRED: <description>

Output format:
FILE: <path>
```cpp
<complete file content>
```'''

SYSTEM_PROMPT_ASK = '''You are an expert assistant for IncludeCPP projects.

You have access to:
- IncludeCPP documentation (README)
- Project source files (.cpp, .h, .py)
- Plugin definitions (.cp files)
- Build system knowledge

Answer questions precisely and concisely. Reference specific files and line numbers when relevant.

Response format:
- Direct answer first
- File references: path:line if applicable
- Code examples if helpful (brief)

Do not explain IncludeCPP basics unless asked. Assume user knows the system.
Max 200 words unless complexity requires more.'''

SYSTEM_PROMPT_EDIT = '''You are a C++ expert editing code in an IncludeCPP project.

CRITICAL INSTRUCTIONS:
- When asked to ADD something (function, class, method, etc.), you MUST add it
- Do NOT say "no changes needed" when the task is to add new code
- If the task says "add X" - you MUST ADD X to the code

Context:
- IncludeCPP generates pybind11 bindings from .cp plugin files
- All exposed code must be in namespace includecpp { }
- Focus on the specific edit task requested

Rules:
1. If task says ADD -> ADD the requested code
2. Preserve all existing functionality
3. No comments, docstrings, or explanatory text
4. Maintain exact coding style
5. Output COMPLETE file content, not just changed parts

If the edit would break existing functionality:
CONFIRM_REQUIRED: <description>

Output format for EACH modified file (REQUIRED):
FILE: <path>
CHANGES:
- Line X: <what changed> - <why>
```cpp
<complete file content>
```

You MUST output at least one FILE: block when changes are requested.'''

SYSTEM_PROMPT_AUTO_FIX = '''You are an AI that automatically fixes C++ build errors in IncludeCPP projects.

IncludeCPP Architecture:
- Plugin files (.cp) define bindings: SOURCE(file.cpp), HEADER(file.h), CLASS(), METHOD(), FUNC(), TEMPLATE_FUNC()
- All exposed code MUST be in namespace includecpp { }
- Build generates pybind11 bindings and compiles to .pyd/.so

You have access to CLI commands:
- includecpp plugin <modulename>: Regenerate .cp file from source analysis
- includecpp rebuild --fast <modulename>: Rebuild specific module
- includecpp rebuild --clean: Full clean rebuild

IMPORTANT: You must FIX the error. Analyze the error, determine the fix, and output actionable changes.

Output format:

ACTION: FILE_CHANGE
FILE: <exact path>
```cpp
<complete fixed file content>
```

ACTION: CLI_COMMAND
COMMAND: <command to run>
REASON: <why this command is needed>

Multiple actions can be output. Process in order:
1. FILE_CHANGE actions first (fix source files)
2. CLI_COMMAND actions after (regenerate .cp or other commands)

Common fixes:
- Syntax error -> Fix the syntax in source file
- Missing namespace -> Wrap code in namespace includecpp { }
- .cp out of sync -> Run: includecpp plugin <modulename>
- Template not detected -> Run: includecpp plugin <modulename> (regenerates with TEMPLATE_FUNC)
- Missing include -> Add #include directive

If the error cannot be fixed automatically, output:
CANNOT_FIX: <explanation>

Do NOT add comments or docstrings. Output complete file contents for FILE_CHANGE.'''

SYSTEM_PROMPT_THINK3_PLAN = '''You are an expert C++ and Python developer analyzing a build error in an IncludeCPP project.

PHASE 1: ANALYSIS (take your time)
- Analyze the exact error message and location
- Identify the root cause (syntax, linker, template, namespace, etc.)
- Consider all possible fixes
- Review relevant documentation and patterns

PHASE 2: RESEARCH FINDINGS
You have access to web research results. Use them to:
- Find similar issues and solutions
- Identify best practices for the specific error
- Check if this is a known issue with pybind11 or compilers

PHASE 3: PLANNING
Create a structured plan:

PLAN:
1. Primary Fix: <main solution>
2. Alternative: <backup approach if primary fails>
3. Files to Modify: <list of files>
4. Commands Needed: <CLI commands if any>
5. Verification: <how to verify the fix worked>

PHASE 4: IMPLEMENTATION
After planning, output the actual fixes using:

ACTION: FILE_CHANGE
FILE: <path>
```cpp
<complete fixed content>
```

ACTION: CLI_COMMAND
COMMAND: <command>
REASON: <why>

Be thorough. This is professional-grade analysis.'''


CLI_KEYWORDS = {
    'plugin': ['def plugin', '@cli.command', 'plugin_name', 'extract_fields', 'extract_methods'],
    'rebuild': ['def rebuild', 'build_manager', '--fast', '--clean', '--auto-ai'],
    'build': ['def build', 'build_manager'],
    'auto': ['def auto', 'auto_plugins', '--all'],
    'fix': ['def fix', 'fix_code', '--ai', 'ai_mgr'],
    'init': ['def init', 'cpp.proj', 'plugins/', 'include/'],
    'ai': ['@ai.command', 'ai_mgr', 'get_ai_manager', 'ai ask', 'ai edit', 'ai optimize'],
    'settings': ['def settings', 'settings_ui', 'PyQt6'],
    'flag': ['@click.option', 'is_flag', '--think', '--websearch', '--confirm'],
    'command': ['@cli.command', '@click.argument', '@click.option'],
}


class AIManager:
    def __init__(self):
        self.secret_dir = Path.home() / '.includecpp'
        self.secret_path = self.secret_dir / '.secret'
        self.config = self._load_config()
        self._doc_cache = None
        self._cli_cache = None

    def _get_documentation(self) -> str:
        if self._doc_cache:
            return self._doc_cache
        try:
            readme_path = Path(__file__).parent.parent.parent / 'README.md'
            if readme_path.exists():
                self._doc_cache = readme_path.read_text(encoding='utf-8')
                return self._doc_cache
        except:
            pass
        return ''

    def _get_build_info(self) -> str:
        """Read build info from AppData for AI context."""
        import platform
        if platform.system() == "Windows":
            appdata = Path(os.environ.get('APPDATA', str(Path.home() / 'AppData' / 'Roaming')))
            build_base = appdata / 'IncludeCPP'
        else:
            build_base = Path.home() / '.includecpp' / 'builds'

        info_parts = []
        if build_base.exists():
            for item in build_base.iterdir():
                if item.is_dir():
                    registry = item / '.module_registry.json'
                    if registry.exists():
                        try:
                            data = json.loads(registry.read_text(encoding='utf-8'))
                            modules = list(data.get('modules', {}).keys())
                            if modules:
                                info_parts.append(f"Project: {item.name}, Modules: {', '.join(modules[:5])}")
                        except:
                            pass

        if info_parts:
            return '\n\nBUILD CONTEXT (user\'s projects):\n' + '\n'.join(info_parts[:5])
        return ''

    def _get_cli_context(self, question: str) -> str:
        """Extract relevant CLI implementation context based on the question.

        Only extracts code sections matching keywords from the question to save tokens.
        """
        question_lower = question.lower()

        # Check if question is about CLI/commands
        cli_terms = ['command', 'flag', 'option', 'cli', 'includecpp', '--', 'how to', 'usage']
        if not any(term in question_lower for term in cli_terms):
            return ''

        # Find which CLI topics are relevant
        relevant_keywords = set()
        for topic, keywords in CLI_KEYWORDS.items():
            if topic in question_lower:
                relevant_keywords.update(keywords)

        # Add generic command keywords if asking about flags/options
        if '--' in question or 'flag' in question_lower or 'option' in question_lower:
            relevant_keywords.update(CLI_KEYWORDS['flag'])

        if not relevant_keywords:
            return ''

        # Load CLI source if not cached
        if self._cli_cache is None:
            try:
                cli_path = Path(__file__).parent.parent / 'cli' / 'commands.py'
                if cli_path.exists():
                    self._cli_cache = cli_path.read_text(encoding='utf-8')
            except:
                return ''

        if not self._cli_cache:
            return ''

        # Extract relevant sections (functions/decorators containing keywords)
        lines = self._cli_cache.split('\n')
        extracted = []
        in_relevant_block = False
        block_lines = []
        indent_level = 0

        for i, line in enumerate(lines):
            stripped = line.strip()

            # Check for function/decorator start
            if stripped.startswith('@') or stripped.startswith('def '):
                # Save previous block if relevant
                if in_relevant_block and block_lines:
                    extracted.extend(block_lines[:50])  # Max 50 lines per block
                    extracted.append('    # ... (truncated)\n')

                # Check if new block is relevant
                in_relevant_block = any(kw in line for kw in relevant_keywords)
                block_lines = [f'{i+1}: {line}'] if in_relevant_block else []
                indent_level = len(line) - len(line.lstrip())

            elif in_relevant_block:
                # Continue block until dedent
                current_indent = len(line) - len(line.lstrip()) if stripped else indent_level + 1
                if stripped and current_indent <= indent_level and not stripped.startswith('@'):
                    # Block ended
                    if block_lines:
                        extracted.extend(block_lines[:50])
                        if len(block_lines) > 50:
                            extracted.append('    # ... (truncated)\n')
                    in_relevant_block = False
                    block_lines = []
                else:
                    block_lines.append(f'{i+1}: {line}')

        # Limit total context
        if extracted:
            context = '\n'.join(extracted[:200])  # Max 200 lines total
            return f'\n\nCLI IMPLEMENTATION (relevant sections):\n```python\n{context}\n```'
        return ''

    def _get_context_limit(self, think: bool = False, think_twice: bool = False,
                           think_three: bool = False) -> int:
        """Get the appropriate context limit based on thinking mode."""
        if think_three:
            return CONTEXT_LIMITS['think3']
        elif think_twice:
            return CONTEXT_LIMITS['think2']
        elif think:
            return CONTEXT_LIMITS['think']
        return CONTEXT_LIMITS['standard']

    def _add_line_numbers(self, content: str) -> str:
        """Add line numbers to source code for AI context."""
        lines = content.split('\n')
        return '\n'.join(f"{i:4d} | {line}" for i, line in enumerate(lines, 1))

    def _categorize_error(self, error: str) -> str:
        """Categorize build error for better AI context."""
        e = error.lower()
        if 'undefined reference' in e or 'unresolved external' in e:
            return 'LINKER_ERROR - Missing definition, check namespace includecpp and FUNC() in .cp'
        elif 'syntax error' in e or 'expected' in e:
            return 'SYNTAX_ERROR - Check for missing semicolons, braces, or typos'
        elif 'namespace' in e:
            return 'NAMESPACE_ERROR - Code likely not wrapped in namespace includecpp { }'
        elif 'template' in e:
            return 'TEMPLATE_ERROR - Check TEMPLATE_FUNC() with TYPES() in .cp file'
        elif 'no matching function' in e or 'no member named' in e:
            return 'SIGNATURE_ERROR - Method signature in .cp doesn\'t match source'
        elif 'include' in e or 'no such file' in e:
            return 'INCLUDE_ERROR - Missing header file or wrong include path'
        return 'UNKNOWN - Analyze error message carefully'

    def _reset_daily_if_needed(self):
        """Reset daily usage if it's a new day."""
        from datetime import date
        today = date.today().isoformat()
        if self.config.get('daily_usage', {}).get('date') != today:
            self.config['daily_usage'] = {'date': today, 'tokens': 0}
            self._save_config()

    def _check_daily_limit(self) -> Tuple[bool, str]:
        """Check if daily limit is exceeded.
        Returns: (can_proceed, warning_message)
        """
        self._reset_daily_if_needed()
        limit = self.config.get('daily_limit', DEFAULT_DAILY_LIMIT)
        used = self.config.get('daily_usage', {}).get('tokens', 0)

        if used >= limit:
            return False, f"Daily token limit reached ({used:,}/{limit:,}). Resets at midnight."
        if used >= limit * 0.8:
            remaining = limit - used
            return True, f"Warning: {remaining:,} tokens remaining today ({int(used/limit*100)}% used)"
        return True, ""

    def get_daily_usage_info(self) -> Dict[str, Any]:
        """Get current daily usage information."""
        self._reset_daily_if_needed()
        limit = self.config.get('daily_limit', DEFAULT_DAILY_LIMIT)
        used = self.config.get('daily_usage', {}).get('tokens', 0)
        return {
            'date': self.config.get('daily_usage', {}).get('date'),
            'tokens_used': used,
            'daily_limit': limit,
            'remaining': max(0, limit - used),
            'percentage': min(100, int(used / limit * 100)) if limit > 0 else 0
        }

    def set_daily_limit(self, limit: int) -> Tuple[bool, str]:
        """Set the daily token limit."""
        if limit < 1000:
            return False, "Daily limit must be at least 1,000 tokens"
        if limit > 10000000:
            return False, "Daily limit cannot exceed 10,000,000 tokens"
        self.config['daily_limit'] = limit
        self._save_config()
        return True, f"Daily limit set to {limit:,} tokens"

    def _load_config(self) -> dict:
        if self.secret_path.exists():
            try:
                data = json.loads(self.secret_path.read_text(encoding='utf-8'))
                if 'usage' not in data:
                    data['usage'] = {'total_tokens': 0, 'total_requests': 0, 'last_request': None}
                if 'daily_limit' not in data:
                    data['daily_limit'] = DEFAULT_DAILY_LIMIT
                if 'daily_usage' not in data:
                    data['daily_usage'] = {'date': None, 'tokens': 0}
                return data
            except (json.JSONDecodeError, IOError):
                pass
        return {
            'api_key': None,
            'brave_api_key': None,
            'enabled': False,
            'model': DEFAULT_MODEL,
            'usage': {
                'total_tokens': 0,
                'total_requests': 0,
                'last_request': None
            },
            'daily_limit': DEFAULT_DAILY_LIMIT,
            'daily_usage': {'date': None, 'tokens': 0}
        }

    def _save_config(self):
        self.secret_dir.mkdir(parents=True, exist_ok=True)
        self.secret_path.write_text(json.dumps(self.config, indent=2), encoding='utf-8')
        if os.name != 'nt':
            os.chmod(self.secret_path, stat.S_IRUSR | stat.S_IWUSR)

    def _mask_key(self) -> str:
        key = self.config.get('api_key')
        if not key:
            return 'Not set'
        if len(key) <= 8:
            return '****'
        return f"{key[:7]}...{key[-4:]}"

    def set_key(self, key: str) -> Tuple[bool, str]:
        if not key:
            return False, 'API key cannot be empty'
        if not key.startswith('sk-'):
            return False, 'Invalid API key format. Key should start with sk-'
        test_result, test_msg = self._test_key(key)
        if not test_result:
            return False, test_msg
        self.config['api_key'] = key
        self._save_config()
        return True, 'API key saved and verified'

    def _test_key(self, key: str) -> Tuple[bool, str]:
        try:
            headers = {
                'Authorization': f'Bearer {key}',
                'Content-Type': 'application/json'
            }
            data = {
                'model': 'gpt-3.5-turbo',
                'messages': [{'role': 'user', 'content': 'test'}],
                'max_tokens': 1
            }
            response = requests.post(OPENAI_API_URL, headers=headers, json=data, timeout=10)
            if response.status_code == 200:
                return True, 'OK'
            elif response.status_code == 401:
                return False, 'Invalid API key'
            elif response.status_code == 429:
                return True, 'OK (rate limited but valid)'
            else:
                return False, f'API error: {response.status_code}'
        except requests.exceptions.Timeout:
            return False, 'Connection timeout'
        except requests.exceptions.ConnectionError:
            return False, 'Connection failed'
        except Exception as e:
            return False, str(e)

    def is_enabled(self) -> bool:
        return bool(self.config.get('enabled', False) and self.config.get('api_key'))

    def has_key(self) -> bool:
        return bool(self.config.get('api_key'))

    def set_brave_key(self, key: str) -> Tuple[bool, str]:
        if not key:
            return False, 'Brave API key cannot be empty'
        test_result, test_msg = self._test_brave_key(key)
        if not test_result:
            return False, test_msg
        self.config['brave_api_key'] = key
        self._save_config()
        return True, 'Brave Search API key saved and verified'

    def _test_brave_key(self, key: str) -> Tuple[bool, str]:
        try:
            headers = {
                'Accept': 'application/json',
                'X-Subscription-Token': key
            }
            response = requests.get(
                f'{BRAVE_SEARCH_URL}?q=test&count=1',
                headers=headers,
                timeout=10
            )
            if response.status_code == 200:
                return True, 'OK'
            elif response.status_code == 401:
                return False, 'Invalid Brave API key'
            elif response.status_code == 429:
                return True, 'OK (rate limited but valid)'
            else:
                return False, f'Brave API error: {response.status_code}'
        except requests.exceptions.Timeout:
            return False, 'Connection timeout'
        except requests.exceptions.ConnectionError:
            return False, 'Connection failed'
        except Exception as e:
            return False, str(e)

    def has_brave_key(self) -> bool:
        return bool(self.config.get('brave_api_key'))

    def brave_search(self, query: str, count: int = 5) -> Tuple[bool, List[Dict]]:
        if not self.has_brave_key():
            return False, []
        try:
            headers = {
                'Accept': 'application/json',
                'X-Subscription-Token': self.config['brave_api_key']
            }
            response = requests.get(
                f'{BRAVE_SEARCH_URL}?q={query}&count={count}',
                headers=headers,
                timeout=15
            )
            if response.status_code == 200:
                data = response.json()
                results = []
                for item in data.get('web', {}).get('results', []):
                    results.append({
                        'title': item.get('title', ''),
                        'url': item.get('url', ''),
                        'description': item.get('description', '')
                    })
                return True, results
            return False, []
        except Exception:
            return False, []

    def enable(self) -> Tuple[bool, str]:
        if not self.config.get('api_key'):
            return False, 'No API key configured. Use: includecpp ai key <YOUR_KEY>'
        self.config['enabled'] = True
        self._save_config()
        return True, 'AI features enabled'

    def disable(self) -> Tuple[bool, str]:
        self.config['enabled'] = False
        self._save_config()
        return True, 'AI features disabled'

    def get_model(self) -> str:
        return self.config.get('model', DEFAULT_MODEL)

    def set_model(self, model: str) -> Tuple[bool, str]:
        if model not in MODELS:
            available = ', '.join(MODELS.keys())
            return False, f'Unknown model. Available: {available}'
        self.config['model'] = model
        self._save_config()
        return True, f'Model set to {model}'

    def list_models(self) -> List[Dict[str, Any]]:
        current = self.get_model()
        result = []
        for name, info in MODELS.items():
            result.append({
                'name': name,
                'context': info['context'],
                'active': name == current
            })
        return result

    def get_info(self) -> Dict[str, Any]:
        usage = self.config.get('usage', {})
        return {
            'key_set': bool(self.config.get('api_key')),
            'key_preview': self._mask_key(),
            'enabled': self.config.get('enabled', False),
            'model': self.config.get('model', DEFAULT_MODEL),
            'total_tokens': usage.get('total_tokens', 0),
            'total_requests': usage.get('total_requests', 0),
            'last_request': usage.get('last_request')
        }

    def _update_usage(self, tokens: int):
        if 'usage' not in self.config:
            self.config['usage'] = {'total_tokens': 0, 'total_requests': 0, 'last_request': None}
        self.config['usage']['total_tokens'] = self.config['usage'].get('total_tokens', 0) + tokens
        self.config['usage']['total_requests'] = self.config['usage'].get('total_requests', 0) + 1
        self.config['usage']['last_request'] = datetime.now().isoformat()
        self._reset_daily_if_needed()
        self.config['daily_usage']['tokens'] = self.config['daily_usage'].get('tokens', 0) + tokens
        self._save_config()

    def query(self, system_prompt: str, user_prompt: str, temperature: float = 0.3,
              timeout: int = 180) -> Tuple[bool, str]:
        if not self.config.get('api_key'):
            return False, 'No API key configured'
        can_proceed, limit_warning = self._check_daily_limit()
        if not can_proceed:
            return False, limit_warning
        model = self.config.get('model', DEFAULT_MODEL)
        model_info = MODELS.get(model, MODELS[DEFAULT_MODEL])
        headers = {
            'Authorization': f'Bearer {self.config["api_key"]}',
            'Content-Type': 'application/json'
        }
        token_limit = min(16000, model_info['context'] // 2)
        data = {
            'model': model_info['endpoint'],
            'messages': [
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': user_prompt}
            ]
        }
        if model.startswith('gpt-5'):
            data['max_completion_tokens'] = token_limit
        else:
            data['max_tokens'] = token_limit
            data['temperature'] = temperature
        try:
            response = requests.post(OPENAI_API_URL, headers=headers, json=data, timeout=timeout)
            if response.status_code == 200:
                result = response.json()
                content = result['choices'][0]['message']['content']
                tokens = result.get('usage', {}).get('total_tokens', 0)
                self._update_usage(tokens)
                return True, content
            elif response.status_code == 401:
                return False, 'Invalid API key'
            elif response.status_code == 429:
                return False, 'Rate limit exceeded. Please wait and try again'
            elif response.status_code == 503:
                return False, 'OpenAI service unavailable'
            else:
                try:
                    err = response.json().get('error', {}).get('message', '')
                except:
                    err = response.text
                return False, f'API error ({response.status_code}): {err}'
        except requests.exceptions.Timeout:
            return False, 'Request timeout'
        except requests.exceptions.ConnectionError:
            return False, 'Connection failed. Check your internet connection'
        except Exception as e:
            return False, str(e)

    def _build_prompt_with_docs(self, base_prompt: str, include_build_info: bool = True) -> str:
        parts = [INCLUDECPP_CONTEXT]
        docs = self._get_documentation()
        if docs:
            doc_section = docs[:8000] if len(docs) > 8000 else docs
            parts.append(f'\nIncludeCPP Documentation:\n{doc_section}')
        if include_build_info:
            build_info = self._get_build_info()
            if build_info:
                parts.append(build_info)
        parts.append(f'\n\n---\n\n{base_prompt}')
        return '\n'.join(parts)

    def optimize_code(self, files: Dict[str, str], custom_task: Optional[str] = None) -> Tuple[bool, str, List[Dict]]:
        if not files:
            return False, 'No files provided', []
        file_content = ''
        for path, content in files.items():
            file_content += f'\nFILE: {path}\n```cpp\n{content}\n```\n'
        if custom_task:
            prompt = f'Task: {custom_task}\n\nFiles:\n{file_content}'
            system = SYSTEM_PROMPT_AGENT
        else:
            prompt = f'Optimize the following C++ files for performance, safety, and pybind11 compatibility:\n{file_content}'
            system = SYSTEM_PROMPT_OPTIMIZE
        prompt = self._build_prompt_with_docs(prompt)
        # v3.2.2: Use longer timeout (5 min) for optimize operations with multiple files
        timeout = 300 if len(files) > 1 else 180
        success, response = self.query(system, prompt, timeout=timeout)
        if not success:
            return False, response, []
        changes = self._parse_file_changes(response)
        return True, response, changes

    def fix_code(self, files: Dict[str, str]) -> Tuple[bool, str, List[Dict]]:
        if not files:
            return False, 'No files provided', []
        file_content = ''
        for path, content in files.items():
            file_content += f'\nFILE: {path}\n```cpp\n{content}\n```\n'
        prompt = f'Analyze and fix issues in these C++ files:\n{file_content}'
        prompt = self._build_prompt_with_docs(prompt)
        success, response = self.query(SYSTEM_PROMPT_FIX, prompt)
        if not success:
            return False, response, []
        changes = self._parse_file_changes(response)
        return True, response, changes

    def analyze_build_error(self, error: str, source_files: Dict[str, str]) -> Tuple[bool, str]:
        context = f'Build error:\n{error}\n\n'
        if source_files:
            context += 'Relevant source files:\n'
            for path, content in list(source_files.items())[:3]:
                lines = content.split('\n')
                preview = '\n'.join(lines[:100])
                context += f'\nFILE: {path}\n```cpp\n{preview}\n```\n'
        context = self._build_prompt_with_docs(context)
        success, response = self.query(SYSTEM_PROMPT_BUILD_ERROR, context)
        return success, response

    def auto_fix_build_error(self, error: str, source_files: Dict[str, str],
                              plugin_content: Optional[str] = None,
                              module_name: Optional[str] = None,
                              think: bool = False,
                              think_twice: bool = False,
                              think_three: bool = False,
                              use_websearch: bool = False) -> Tuple[bool, str, List[Dict], List[Dict]]:
        error_category = self._categorize_error(error)
        context = f'Build error:\n{error}\n\nError category: {error_category}\n\n'
        if module_name:
            context += f'Module name: {module_name}\n\n'
        if use_websearch and self.has_brave_key():
            search_query = f'C++ pybind11 {error[:100]}'
            success, results = self.brave_search(search_query, count=5)
            if success and results:
                context += 'Web Research Results:\n'
                for r in results:
                    context += f'- {r["title"]}: {r["description"][:200]}\n'
                context += '\n'
        if source_files:
            context += 'Source files (with line numbers):\n'
            max_lines = self._get_context_limit(think, think_twice, think_three)
            for path, content in source_files.items():
                lines = content.split('\n')
                if len(lines) > max_lines:
                    preview = '\n'.join(lines[:max_lines])
                    numbered = self._add_line_numbers(preview)
                    context += f'\nFILE: {path}\n```cpp\n{numbered}\n... ({len(lines) - max_lines} more lines)\n```\n'
                else:
                    numbered = self._add_line_numbers(content)
                    context += f'\nFILE: {path}\n```cpp\n{numbered}\n```\n'
        if plugin_content:
            context += f'\nPlugin definition (.cp file):\n```\n{plugin_content}\n```\n'
        if think_three:
            context += '\nIMPORTANT: This is professional-grade analysis. Take your time to plan thoroughly before implementing.'
        elif think_twice:
            context += '\nIMPORTANT: Analyze thoroughly. This is a complex codebase requiring careful consideration.'
        elif think:
            context += '\nIMPORTANT: Think step by step before fixing.'
        context = self._build_prompt_with_docs(context)
        system_prompt = SYSTEM_PROMPT_THINK3_PLAN if think_three else SYSTEM_PROMPT_AUTO_FIX
        temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
        success, response = self.query(system_prompt, context, temperature=temperature)
        if not success:
            return False, response, [], []
        file_changes, cli_commands = self._parse_auto_fix_response(response)
        return True, response, file_changes, cli_commands

    def _parse_auto_fix_response(self, response: str) -> Tuple[List[Dict], List[Dict]]:
        file_changes = []
        cli_commands = []
        lines = response.split('\n')
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if line == 'ACTION: FILE_CHANGE':
                i += 1
                if i < len(lines) and lines[i].strip().startswith('FILE:'):
                    file_path = lines[i].strip()[5:].strip()
                    i += 1
                    content_lines = []
                    in_code = False
                    while i < len(lines):
                        l = lines[i]
                        if l.strip().startswith('```cpp'):
                            in_code = True
                            i += 1
                            continue
                        if in_code and l.strip().startswith('```'):
                            break
                        if in_code:
                            content_lines.append(l)
                        i += 1
                    if content_lines:
                        file_changes.append({
                            'file': file_path,
                            'content': '\n'.join(content_lines)
                        })
            elif line == 'ACTION: CLI_COMMAND':
                i += 1
                command = None
                reason = None
                while i < len(lines):
                    l = lines[i].strip()
                    if l.startswith('COMMAND:'):
                        command = l[8:].strip()
                    elif l.startswith('REASON:'):
                        reason = l[7:].strip()
                    elif l.startswith('ACTION:') or l.startswith('CANNOT_FIX:'):
                        break
                    i += 1
                    if command and reason:
                        break
                if command:
                    cli_commands.append({
                        'command': command,
                        'reason': reason or ''
                    })
                continue
            elif line.startswith('CANNOT_FIX:'):
                break
            i += 1
        return file_changes, cli_commands

    def ask_question(self, question: str, files: Dict[str, str], plugins: Dict[str, str] = None,
                      think: bool = False, think_twice: bool = False, think_three: bool = False,
                      use_websearch: bool = False) -> Tuple[bool, str]:
        context_parts = []
        if use_websearch and self.has_brave_key():
            search_query = f'C++ pybind11 {question[:100]}'
            success, results = self.brave_search(search_query, count=5)
            if success and results:
                context_parts.append('Web Research Results:')
                for r in results:
                    context_parts.append(f'- {r["title"]}: {r["description"][:200]}')
                context_parts.append('')
        max_lines = self._get_context_limit(think, think_twice, think_three)
        if files:
            context_parts.append('Project files:')
            for path, content in files.items():
                lines = content.split('\n')
                if len(lines) > max_lines:
                    preview = '\n'.join(lines[:max_lines])
                    preview += f'\n... ({len(lines) - max_lines} more lines)'
                else:
                    preview = content
                ext = Path(path).suffix
                lang = 'cpp' if ext in ['.cpp', '.h', '.hpp', '.c'] else 'python' if ext == '.py' else ''
                context_parts.append(f'\nFILE: {path}\n```{lang}\n{preview}\n```')
        if plugins:
            context_parts.append('\nPlugin definitions:')
            for path, content in plugins.items():
                context_parts.append(f'\nPLUGIN: {path}\n```\n{content}\n```')
        # v3.2.2: Add CLI context for questions about commands/flags
        cli_context = self._get_cli_context(question)
        if cli_context:
            context_parts.append(cli_context)
        context = '\n'.join(context_parts)
        prompt = f'Question: {question}\n\n{context}'
        if think_three:
            prompt += '\n\nIMPORTANT: Provide thorough, professional-grade analysis with detailed reasoning.'
        elif think_twice:
            prompt += '\n\nIMPORTANT: Analyze carefully and consider multiple angles before answering.'
        elif think:
            prompt += '\n\nIMPORTANT: Think step by step before answering.'
        prompt = self._build_prompt_with_docs(prompt)
        temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
        return self.query(SYSTEM_PROMPT_ASK, prompt, temperature=temperature)

    def edit_code(self, task: str, files: Dict[str, str], think: bool = False,
                   think_twice: bool = False, think_three: bool = False,
                   use_websearch: bool = False) -> Tuple[bool, str, List[Dict]]:
        if not files:
            return False, 'No files provided', []
        context_parts = []
        if use_websearch and self.has_brave_key():
            search_query = f'C++ pybind11 {task[:100]}'
            success, results = self.brave_search(search_query, count=5)
            if success and results:
                context_parts.append('Web Research Results:')
                for r in results:
                    context_parts.append(f'- {r["title"]}: {r["description"][:200]}')
                context_parts.append('')
        max_lines = self._get_context_limit(think, think_twice, think_three)
        file_content = ''
        for path, content in files.items():
            lines = content.split('\n')
            if len(lines) > max_lines:
                preview = '\n'.join(lines[:max_lines])
                preview += f'\n... ({len(lines) - max_lines} more lines)'
            else:
                preview = content
            ext = Path(path).suffix
            lang = 'cpp' if ext in ['.cpp', '.h', '.hpp', '.c'] else 'python' if ext == '.py' else ''
            file_content += f'\nFILE: {path}\n```{lang}\n{preview}\n```\n'
        prompt = '\n'.join(context_parts) + f'Edit task: {task}\n\nFiles:\n{file_content}'
        if think_three:
            prompt += '\n\nIMPORTANT: This is professional-grade editing. Plan thoroughly, consider all implications, and implement carefully.'
        elif think_twice:
            prompt += '\n\nIMPORTANT: Think carefully before making changes. Consider edge cases and potential issues.'
        elif think:
            prompt += '\n\nIMPORTANT: Think step by step before editing.'
        prompt = self._build_prompt_with_docs(prompt)
        temperature = 0.1 if think_three else (0.2 if think_twice else 0.3)
        success, response = self.query(SYSTEM_PROMPT_EDIT, prompt, temperature=temperature)
        if not success:
            return False, response, []
        changes = self._parse_file_changes(response)
        return True, response, changes

    def _parse_file_changes(self, response: str) -> List[Dict]:
        import re
        lower_response = response.lower()
        if any(p in lower_response for p in [
            'no_changes_needed', 'no_issues_found', 'no changes are needed',
            'no changes necessary', 'code looks good', 'no modifications required'
        ]):
            return []

        changes = []
        lines = response.split('\n')
        current_file = None
        current_content = []
        current_changes_desc = []
        in_code_block = False
        in_changes_section = False
        confirm_required = []
        for line in lines:
            if line.startswith('CONFIRM_REQUIRED:'):
                confirm_required.append(line[17:].strip())
                continue
            if line.startswith('FILE:') or line.startswith('**FILE:**') or line.startswith('### FILE:'):
                if current_file and current_content:
                    changes.append({
                        'file': current_file,
                        'content': '\n'.join(current_content),
                        'changes_desc': current_changes_desc.copy(),
                        'confirm_required': confirm_required.copy()
                    })
                    confirm_required = []
                file_line = line
                for prefix in ['FILE:', '**FILE:**', '### FILE:', '**FILE**:']:
                    file_line = file_line.replace(prefix, '')
                current_file = file_line.strip().strip('`').strip('*')
                current_content = []
                current_changes_desc = []
                in_code_block = False
                in_changes_section = False
                continue
            if line.strip() in ['CHANGES:', '**CHANGES:**', '### CHANGES:']:
                in_changes_section = True
                continue
            if in_changes_section and not in_code_block:
                stripped = line.strip()
                if stripped.startswith('- ') or stripped.startswith('* '):
                    current_changes_desc.append(stripped[2:])
                elif stripped.startswith('```'):
                    in_changes_section = False
                    in_code_block = True
                continue
            if line.strip().startswith('```cpp') or line.strip().startswith('```c++'):
                in_code_block = True
                in_changes_section = False
                continue
            if line.strip() == '```' and in_code_block:
                in_code_block = False
                continue
            if in_code_block and current_file:
                current_content.append(line)
        if current_file and current_content:
            changes.append({
                'file': current_file,
                'content': '\n'.join(current_content),
                'changes_desc': current_changes_desc,
                'confirm_required': confirm_required
            })

        if not changes and '```cpp' in response:
            changes = self._fallback_parse_code_blocks(response)

        return changes

    def _fallback_parse_code_blocks(self, response: str) -> List[Dict]:
        """Fallback parser for responses that don't follow the exact format."""
        import re
        changes = []
        blocks = re.findall(r'```(?:cpp|c\+\+)?\n(.*?)```', response, re.DOTALL)
        file_pattern = r'(?:file|path|in|modify|update)[:\s]*[`"]?([^\s`"]+\.(?:cpp|h|hpp|c))[`"]?'
        file_matches = re.findall(file_pattern, response, re.IGNORECASE)

        for idx, content in enumerate(blocks):
            file_name = file_matches[idx] if idx < len(file_matches) else f'file_{idx}.cpp'
            if content.strip():
                changes.append({
                    'file': file_name,
                    'content': content.strip(),
                    'changes_desc': ['Parsed from code block (fallback)'],
                    'confirm_required': []
                })
        return changes


_ai_manager_instance = None


def get_ai_manager() -> AIManager:
    global _ai_manager_instance
    if _ai_manager_instance is None:
        _ai_manager_instance = AIManager()
    return _ai_manager_instance
