"""
CARL Control Panel - Flask Server 
A desktop settings UI for managing CARL configuration.
"""
from flask import Flask, render_template, jsonify, request
import json
import re
import shutil
from pathlib import Path

from .api import database
from .api.database import (
    init_database, get_active_profile, get_all_profiles, get_profile_by_id,
    create_profile, switch_profile, delete_profile, update_profile,
    get_setting, set_setting
)
from .api.manifest import ManifestManager
from .api.scaffold import create_carl_installation, get_suggested_workspace, find_existing_carl_installations
from .api.hooks import (
    check_hooks_status, install_hooks, get_settings_json_snippet, get_claude_prompt, auto_update_settings,
    check_optional_hooks_status, install_optional_hook, uninstall_optional_hook,
    wire_optional_hook, get_optional_hook_snippet, toggle_optional_hook,
    check_hook_versions, update_hook, update_all_hooks, BUNDLED_HOOK_VERSIONS
)
from .api.sessions import SessionManager, get_available_toggles
from .api import domains as domains_api
from .api.addons import AddonsManager
from .api import resources as resources_api
from .api.cleanup import (
    disable_carl_hooks, uninstall_carl_hooks, cleanup_carl_full,
    get_cleanup_preview, cleanup_addon_full
)
from .api.updater import (
    get_version, get_update_status, check_for_updates, check_for_updates_async,
    download_update_async, apply_update, reset_update_state
)

BASE_DIR = Path(__file__).parent.resolve()
app = Flask(__name__,
            static_folder=str(BASE_DIR / 'static'),
            template_folder=str(BASE_DIR / 'templates'))
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0  # Disable static file caching for dev

# Global state (initialized on startup)
manifest_manager = None
session_manager = None
addons_manager = None
current_profile = None


def load_active_profile():
    """Load workspace from active profile (no auto-detection)."""
    global manifest_manager, session_manager, addons_manager, current_profile

    manifest_manager = None
    session_manager = None
    addons_manager = None
    current_profile = None

    profile = get_active_profile()
    if not profile:
        return

    # Always set current_profile (even if .carl doesn't exist yet)
    current_profile = profile

    # Only initialize managers if CARL is fully configured
    carl_path = Path(profile['workspace_path']) / profile['carl_subpath']
    if carl_path.exists() and (carl_path / 'manifest').exists():
        manifest_manager = ManifestManager(carl_path)
        session_manager = SessionManager(carl_path)
        # Initialize addons manager with bundled addons path
        bundled_addons = BASE_DIR / 'static' / 'addons'
        addons_manager = AddonsManager(carl_path, bundled_addons)


# ---- Page Routes ----

@app.route('/')
def index():
    return render_template('index.html')


# ---- Status API ----

@app.route('/api/status', methods=['GET'])
def api_get_status():
    """Get current status and connection info."""
    profiles = get_all_profiles()
    has_profiles = len(profiles) > 0

    if manifest_manager and manifest_manager.exists():
        return jsonify({
            'connected': True,
            'has_profiles': has_profiles,
            'profile': current_profile,
            'info': manifest_manager.get_carl_info()
        })

    # Check if active profile exists but .carl is incomplete
    if current_profile:
        carl_path = Path(current_profile['workspace_path']) / current_profile['carl_subpath']
        return jsonify({
            'connected': False,
            'has_profiles': has_profiles,
            'profile': current_profile,
            'needs_init': True,  # Has profile but .carl needs initialization
            'workspace_path': current_profile['workspace_path'],
            'info': None
        })

    return jsonify({
        'connected': False,
        'has_profiles': has_profiles,
        'profile': None,
        'needs_workspace': True,  # No profile selected
        'info': None
    })


# ---- Toggles API ----

@app.route('/api/toggles', methods=['GET'])
def api_get_toggles():
    """Get all toggle values."""
    if not manifest_manager:
        return jsonify({'error': 'No CARL installation found'})

    return jsonify({
        'success': True,
        'toggles': manifest_manager.read_toggles()
    })


@app.route('/api/toggles', methods=['POST'])
def api_save_toggles():
    """Save toggle values to manifest."""
    if not manifest_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        toggles = data.get('toggles', {})
        success = manifest_manager.write_toggles(toggles)
        return jsonify({'success': success})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/toggles/<key>', methods=['POST'])
def api_save_single_toggle(key):
    """Save a single toggle value."""
    if not manifest_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    data = request.get_json()
    value = data.get('value', False)
    success = manifest_manager.write_toggle(key, value)
    return jsonify({'success': success})


# ---- Workspace/Profile Management API ----

@app.route('/api/workspaces', methods=['GET'])
def api_get_workspaces():
    """Get all configured workspaces/profiles."""
    profiles = get_all_profiles()

    # Add validation status to each profile
    for profile in profiles:
        carl_path = Path(profile['workspace_path']) / profile['carl_subpath']
        profile['carl_exists'] = carl_path.exists()
        profile['manifest_exists'] = (carl_path / 'manifest').exists() if carl_path.exists() else False
        profile['is_valid'] = profile['carl_exists'] and profile['manifest_exists']

        # Optional files status
        profile['has_global'] = (carl_path / 'global').exists() if carl_path.exists() else False
        profile['has_context'] = (carl_path / 'context').exists() if carl_path.exists() else False
        profile['missing_optional'] = not profile['has_global'] or not profile['has_context']

    return jsonify({
        'success': True,
        'workspaces': profiles,
        'active_id': current_profile['id'] if current_profile else None
    })


@app.route('/api/workspaces', methods=['POST'])
def api_add_workspace():
    """Add a new workspace profile."""
    data = request.get_json()
    name = data.get('name', '')
    path = data.get('path', '')

    workspace_path = Path(path)

    if not workspace_path.exists():
        return jsonify({'success': False, 'error': 'Path does not exist'})

    # Check if this workspace is already registered
    existing = get_all_profiles()
    for profile in existing:
        if profile['workspace_path'] == str(workspace_path):
            return jsonify({'success': False, 'error': 'Workspace already registered'})

    # Create profile (set as active)
    profile_id = create_profile(name, str(workspace_path), '.carl', set_active=True)

    # Reload to activate this workspace
    load_active_profile()

    return jsonify({
        'success': True,
        'profile_id': profile_id,
        'needs_init': not (workspace_path / '.carl' / 'manifest').exists()
    })


@app.route('/api/workspaces/switch', methods=['POST'])
def api_switch_workspace():
    """Switch to a different workspace."""
    data = request.get_json()
    profile_id = data.get('profile_id', '')

    if not switch_profile(profile_id):
        return jsonify({'success': False, 'error': 'Profile not found'})

    # Reload to activate
    load_active_profile()

    is_connected = manifest_manager is not None and manifest_manager.exists()
    return jsonify({
        'success': True,
        'connected': is_connected,
        'profile': current_profile,
        'workspace_path': current_profile['workspace_path'] if current_profile and not is_connected else None,
        'info': manifest_manager.get_carl_info() if manifest_manager else None
    })


@app.route('/api/workspaces/<profile_id>', methods=['DELETE'])
def api_remove_workspace(profile_id):
    """Remove a workspace profile (does not delete .carl files)."""
    profiles = get_all_profiles()
    if len(profiles) == 1:
        return jsonify({'success': False, 'error': 'Cannot remove the only workspace'})

    was_active = current_profile and current_profile['id'] == profile_id

    if not delete_profile(profile_id):
        return jsonify({'success': False, 'error': 'Profile not found'})

    # If we removed the active profile, switch to another
    if was_active:
        remaining = get_all_profiles()
        if remaining:
            switch_profile(remaining[0]['id'])
        load_active_profile()

    return jsonify({'success': True})


@app.route('/api/workspaces/set', methods=['POST'])
def api_set_workspace():
    """Legacy method - add and switch to workspace."""
    data = request.get_json()
    path = data.get('path', '')
    workspace_path = Path(path)

    # Validate path has .carl
    carl_path = workspace_path / '.carl'
    if not carl_path.exists() or not (carl_path / 'manifest').exists():
        return jsonify({'success': False, 'error': 'No valid .carl installation at this path'})

    # Check if already registered
    existing = get_all_profiles()
    for profile in existing:
        if profile['workspace_path'] == str(workspace_path):
            # Already exists - just switch to it
            return api_switch_workspace_internal(profile['id'])

    # Add as new workspace
    name = workspace_path.name or 'Workspace'
    return api_add_workspace_internal(name, str(workspace_path))


def api_switch_workspace_internal(profile_id):
    """Internal helper for switch workspace."""
    if not switch_profile(profile_id):
        return jsonify({'success': False, 'error': 'Profile not found'})
    load_active_profile()
    return jsonify({
        'success': True,
        'connected': manifest_manager is not None and manifest_manager.exists(),
        'profile': current_profile,
        'info': manifest_manager.get_carl_info() if manifest_manager else None
    })


def api_add_workspace_internal(name, path):
    """Internal helper for add workspace."""
    workspace_path = Path(path)
    profile_id = create_profile(name, str(workspace_path), '.carl', set_active=True)
    load_active_profile()
    return jsonify({
        'success': True,
        'profile_id': profile_id,
        'needs_init': not (workspace_path / '.carl' / 'manifest').exists()
    })


@app.route('/api/suggested-path', methods=['GET'])
def api_get_suggested_path():
    """Get suggested workspace path for new CARL installation."""
    return jsonify({
        'path': get_suggested_workspace()
    })


@app.route('/api/browse-folder', methods=['POST'])
def api_browse_folder():
    """Open native folder picker dialog and return selected path."""
    import threading
    result = {'path': None}

    def pick_folder():
        try:
            import tkinter as tk
            from tkinter import filedialog
            root = tk.Tk()
            root.withdraw()  # Hide the main window
            root.attributes('-topmost', True)  # Bring dialog to front
            folder = filedialog.askdirectory(
                title="Select Workspace Folder"
            )
            root.destroy()
            result['path'] = folder if folder else None
        except Exception as e:
            result['error'] = str(e)

    # Run in thread to avoid blocking
    thread = threading.Thread(target=pick_folder)
    thread.start()
    thread.join(timeout=60)  # Wait up to 60 seconds

    return jsonify(result)


@app.route('/api/workspace/check', methods=['POST'])
def api_check_workspace():
    """Check workspace status - does it have .claude and .carl?"""
    data = request.get_json()
    path = data.get('path', '')
    workspace_path = Path(path)

    if not workspace_path.exists() or not workspace_path.is_dir():
        return jsonify({'valid': False, 'error': 'Invalid directory path'})

    claude_path = workspace_path / '.claude'
    carl_path = workspace_path / '.carl'

    return jsonify({
        'valid': True,
        'has_claude': claude_path.exists(),
        'has_carl': carl_path.exists() and (carl_path / 'manifest').exists(),
        'path': str(workspace_path)
    })


@app.route('/api/workspace/bootstrap-claude', methods=['POST'])
def api_bootstrap_claude():
    """Create minimal .claude structure for CARL to use."""
    data = request.get_json()
    path = data.get('path', '')
    workspace_path = Path(path)

    if not workspace_path.exists() or not workspace_path.is_dir():
        return jsonify({'success': False, 'error': 'Invalid directory path'})

    claude_path = workspace_path / '.claude'
    hooks_path = claude_path / 'hooks'
    settings_path = claude_path / 'settings.json'

    try:
        # Create .claude directory
        claude_path.mkdir(exist_ok=True)

        # Create hooks directory
        hooks_path.mkdir(exist_ok=True)

        # Create minimal settings.json if it doesn't exist
        if not settings_path.exists():
            import json
            minimal_settings = {
                "hooks": {}
            }
            settings_path.write_text(json.dumps(minimal_settings, indent=2))

        return jsonify({
            'success': True,
            'created': {
                'claude_dir': str(claude_path),
                'hooks_dir': str(hooks_path),
                'settings_file': str(settings_path) if not settings_path.exists() else None
            }
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/browse-file', methods=['POST'])
def api_browse_file():
    """Open native file picker dialog and return selected path."""
    import threading
    data = request.get_json() or {}
    file_filter = data.get('filter', '')  # e.g., 'CLAUDE.md'
    result = {'path': None}

    def pick_file():
        try:
            import tkinter as tk
            from tkinter import filedialog
            root = tk.Tk()
            root.withdraw()
            root.attributes('-topmost', True)

            filetypes = [("All files", "*.*")]
            if file_filter:
                filetypes.insert(0, (file_filter, file_filter))

            file_path = filedialog.askopenfilename(
                title=f"Select {file_filter}" if file_filter else "Select File",
                filetypes=filetypes
            )
            root.destroy()
            result['path'] = file_path if file_path else None
        except Exception as e:
            result['error'] = str(e)

    thread = threading.Thread(target=pick_file)
    thread.start()
    thread.join(timeout=60)

    return jsonify(result)


@app.route('/api/existing-installations', methods=['GET'])
def api_get_existing_installations():
    """Get list of existing CARL installations in common locations."""
    installations = find_existing_carl_installations()

    # Add current workspace if connected
    current_path = None
    if manifest_manager and manifest_manager.exists():
        current_path = str(manifest_manager.carl_path.parent)

    return jsonify({
        'installations': installations,
        'current_path': current_path
    })


@app.route('/api/workspace/path', methods=['GET'])
def api_get_current_workspace_path():
    """Get the current workspace path (for prefilling change workspace form)."""
    if manifest_manager and manifest_manager.exists():
        return jsonify({
            'path': str(manifest_manager.carl_path.parent),
            'connected': True
        })
    return jsonify({
        'path': None,
        'connected': False
    })


@app.route('/api/optional-files', methods=['POST'])
def api_create_optional_files():
    """Create optional global/context files for current workspace."""
    if not manifest_manager or not current_profile:
        return jsonify({'success': False, 'error': 'No workspace active'})

    carl_path = manifest_manager.carl_path
    created = []

    try:
        # Create global if missing
        global_path = carl_path / 'global'
        if not global_path.exists():
            global_content = """# GLOBAL Domain Rules
# These rules apply universally to all sessions.

GLOBAL_RULE_0=GLOBAL rules apply universally to all sessions and contexts.
GLOBAL_RULE_1=Use absolute paths in programming to prevent path traversal issues.
GLOBAL_RULE_2=Batch tool calls when possible, sequential only for dependencies.
"""
            global_path.write_text(global_content)
            created.append('global')

        # Create context if missing
        context_path = carl_path / 'context'
        if not context_path.exists():
            context_content = """# CONTEXT Domain Rules
# Context-aware behaviors and memory management.

CONTEXT_RULE_0=CONTEXT rules govern memory, state, and context preservation.
"""
            context_path.write_text(context_content)
            created.append('context')

        return jsonify({'success': True, 'created': created})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/settings/optional-prompt-dismissed', methods=['GET'])
def api_get_optional_prompt_dismissed():
    """Check if optional files prompt was dismissed."""
    dismissed = get_setting('optional_files_dismissed', 'false') == 'true'
    return jsonify({'dismissed': dismissed})


@app.route('/api/settings/optional-prompt-dismissed', methods=['POST'])
def api_dismiss_optional_prompt():
    """Permanently dismiss the optional files prompt."""
    set_setting('optional_files_dismissed', 'true')
    return jsonify({'success': True})


@app.route('/api/initialize', methods=['POST'])
def api_initialize_carl():
    """Create a new CARL installation at the given path or current profile's path."""
    global manifest_manager, session_manager

    data = request.get_json()
    workspace_path = data.get('path')
    force = data.get('force', False)  # Force reinit if True

    # Use provided path or current profile's workspace
    if workspace_path is None and current_profile:
        workspace_path = current_profile['workspace_path']

    if not workspace_path:
        return jsonify({'success': False, 'error': 'No workspace path specified'})

    result = create_carl_installation(workspace_path, force=force)

    if result['success']:
        # Set up the manifest manager for the new installation
        carl_path = Path(workspace_path) / '.carl'
        manifest_manager = ManifestManager(carl_path)
        session_manager = SessionManager(carl_path)
        result['info'] = manifest_manager.get_carl_info()

    return jsonify(result)


# ---- Hooks Management API ----

@app.route('/api/hooks/status', methods=['GET'])
def api_get_hooks_status():
    """Get status of installed hooks."""
    # Pass workspace path so we check both global and workspace hooks
    workspace_path = None
    if manifest_manager and manifest_manager.carl_path:
        workspace_path = manifest_manager.carl_path.parent

    return jsonify(check_hooks_status(workspace_path))


@app.route('/api/hooks/install', methods=['POST'])
def api_install_hooks():
    """Install bundled hooks to specified location."""
    data = request.get_json() or {}
    location = data.get('location', 'global')

    # Get workspace path for project-level installation
    workspace_path = None
    if location == 'project' and manifest_manager and manifest_manager.carl_path:
        workspace_path = manifest_manager.carl_path.parent

    return jsonify(install_hooks(location=location, workspace_path=workspace_path))


@app.route('/api/hooks/snippet', methods=['GET'])
def api_get_hooks_snippet():
    """Get the settings.json configuration snippet for hooks."""
    return jsonify({
        'snippet': get_settings_json_snippet()
    })


@app.route('/api/hooks/prompt', methods=['GET'])
def api_get_hooks_prompt():
    """Get a prompt to give Claude Code for wiring hooks."""
    return jsonify({
        'prompt': get_claude_prompt()
    })


@app.route('/api/hooks/wire', methods=['POST'])
def api_auto_wire_hooks():
    """Automatically update ~/.claude/settings.json with hooks config."""
    return jsonify(auto_update_settings())


# ---- CLAUDE.md Integration API ----

CARL_MARKER_START = '<!-- CARL-MANAGED: Do not remove this section -->'
CARL_MARKER_END = '<!-- END CARL-MANAGED -->'

CARL_CLAUDEMD_BLOCK = f"""{CARL_MARKER_START}
## CARL Integration

Follow all rules in <carl-rules> blocks from system-reminders.
These are dynamically injected based on context and MUST be obeyed.
{CARL_MARKER_END}
"""


def get_claudemd_path(workspace_path):
    """Get the path to CLAUDE.md for a workspace.

    Checks in order:
    1. workspace/.claude/CLAUDE.md (project-level)
    2. workspace/CLAUDE.md (root-level)
    3. ~/.claude/CLAUDE.md (global)
    """
    # Check workspace/.claude/CLAUDE.md first (project-level)
    project_path = Path(workspace_path) / '.claude' / 'CLAUDE.md'
    if project_path.exists():
        return project_path

    # Check workspace/CLAUDE.md (root-level)
    root_path = Path(workspace_path) / 'CLAUDE.md'
    if root_path.exists():
        return root_path

    # Check global ~/.claude/CLAUDE.md
    global_path = Path.home() / '.claude' / 'CLAUDE.md'
    if global_path.exists():
        return global_path

    # Return global path as default (most common location)
    return global_path


def check_claudemd_carl_status(workspace_path):
    """Check if CLAUDE.md exists and contains CARL integration block."""
    claudemd_path = get_claudemd_path(workspace_path)
    return check_claudemd_carl_status_with_path(claudemd_path)


def check_claudemd_carl_status_with_path(claudemd_path):
    """Check if CLAUDE.md at given path exists and contains CARL integration block."""
    result = {
        'exists': claudemd_path.exists(),
        'path': str(claudemd_path),
        'has_carl_block': False,
        'status': 'missing'  # missing, needs_carl, configured
    }

    if not claudemd_path.exists():
        result['status'] = 'missing'
        return result

    content = claudemd_path.read_text(encoding='utf-8')
    if CARL_MARKER_START in content and CARL_MARKER_END in content:
        result['has_carl_block'] = True
        result['status'] = 'configured'
    else:
        result['status'] = 'needs_carl'

    return result


@app.route('/api/claudemd/status', methods=['GET'])
def api_get_claudemd_status():
    """Check if CLAUDE.md exists and has CARL integration block."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'error': 'No workspace active'})

    # Check for custom path override
    custom_path = get_setting('claudemd_custom_path')
    if custom_path:
        result = check_claudemd_carl_status_with_path(Path(custom_path))
    else:
        workspace_path = manifest_manager.carl_path.parent
        result = check_claudemd_carl_status(workspace_path)

    # Check if dismissed - but auto-clear if CARL block is missing
    # (so user sees prompt again if they remove the block)
    dismissed = get_setting('claudemd_dismissed') == 'true'
    if dismissed and result['status'] != 'configured':
        # CARL block was removed - clear dismissed state
        set_setting('claudemd_dismissed', 'false')
        dismissed = False

    result['dismissed'] = dismissed
    return jsonify(result)


@app.route('/api/claudemd/dismiss', methods=['POST'])
def api_claudemd_dismiss():
    """Dismiss the CLAUDE.md integration prompt."""
    set_setting('claudemd_dismissed', 'true')
    return jsonify({'success': True})


@app.route('/api/claudemd/set-path', methods=['POST'])
def api_claudemd_set_path():
    """Set a custom CLAUDE.md path."""
    data = request.get_json()
    path = data.get('path')

    if not path:
        return jsonify({'success': False, 'error': 'No path provided'})

    # Validate path exists or can be created
    path_obj = Path(path)
    if not path_obj.exists() and not path_obj.parent.exists():
        return jsonify({'success': False, 'error': 'Invalid path'})

    set_setting('claudemd_custom_path', path)
    # Clear dismissed when path changes
    set_setting('claudemd_dismissed', 'false')
    return jsonify({'success': True, 'path': path})


@app.route('/api/claudemd/auto-append', methods=['POST'])
def api_claudemd_auto_append():
    """Append CARL integration block to existing CLAUDE.md."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    # Use custom path if set, otherwise auto-detect
    custom_path = get_setting('claudemd_custom_path')
    if custom_path:
        claudemd_path = Path(custom_path)
    else:
        workspace_path = manifest_manager.carl_path.parent
        claudemd_path = get_claudemd_path(workspace_path)

    try:
        if claudemd_path.exists():
            # Read existing content
            content = claudemd_path.read_text(encoding='utf-8')

            # Check if already has CARL block
            if CARL_MARKER_START in content:
                return jsonify({
                    'success': False,
                    'error': 'CARL integration block already exists'
                })

            # Append CARL block with newlines
            new_content = content.rstrip() + '\n\n' + CARL_CLAUDEMD_BLOCK
        else:
            # Create new file with CARL block
            claudemd_path.parent.mkdir(parents=True, exist_ok=True)
            new_content = f"# CLAUDE.md\n\nProject-level instructions for Claude Code.\n\n{CARL_CLAUDEMD_BLOCK}"

        claudemd_path.write_text(new_content, encoding='utf-8')

        return jsonify({
            'success': True,
            'path': str(claudemd_path),
            'created': not claudemd_path.exists()
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/claudemd/snippet', methods=['GET'])
def api_get_claudemd_snippet():
    """Get the CARL integration markdown block for manual copy."""
    return jsonify({
        'snippet': CARL_CLAUDEMD_BLOCK
    })


@app.route('/api/claudemd/prompt', methods=['GET'])
def api_get_claudemd_prompt():
    """Get a prompt to give Claude Code for adding CARL integration."""
    if not manifest_manager or not manifest_manager.carl_path:
        workspace_hint = "my workspace"
    else:
        workspace_hint = str(manifest_manager.carl_path.parent)

    prompt = f"""Please add CARL integration to my CLAUDE.md file.

Check if {workspace_hint}/.claude/CLAUDE.md exists:
- If it exists, append the CARL integration block to the end
- If it doesn't exist, create it with the CARL integration block

Here's the block to add:

{CARL_CLAUDEMD_BLOCK}

This enables CARL to inject dynamic rules that Claude Code will follow."""

    return jsonify({'prompt': prompt})


# ---- Optional Hooks Management API ----

@app.route('/api/hooks/optional', methods=['GET'])
def api_get_optional_hooks_status():
    """Get status of optional hooks (three-state: not_installed, installed, wired)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'error': 'No workspace active', 'hooks': []})

    try:
        workspace_path = manifest_manager.carl_path.parent
        result = check_optional_hooks_status(workspace_path, manifest_manager)
        return jsonify(result)
    except Exception as e:
        return jsonify({'error': f'Failed to check optional hooks: {str(e)}', 'hooks': []})


@app.route('/api/hooks/optional/install', methods=['POST'])
def api_install_optional_hook():
    """Install an optional hook file to workspace (does NOT wire)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    hook_name = data.get('hook_name', '')
    workspace_path = manifest_manager.carl_path.parent
    return jsonify(install_optional_hook(hook_name, workspace_path))


@app.route('/api/hooks/optional/wire', methods=['POST'])
def api_wire_optional_hook():
    """Wire an installed optional hook to settings.json."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    hook_name = data.get('hook_name', '')
    workspace_path = manifest_manager.carl_path.parent
    return jsonify(wire_optional_hook(hook_name, workspace_path))


@app.route('/api/hooks/optional/snippet', methods=['GET'])
def api_get_optional_hook_snippet():
    """Get settings.json snippet for manual wiring."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    hook_name = request.args.get('hook_name', '')
    workspace_path = manifest_manager.carl_path.parent
    return jsonify(get_optional_hook_snippet(hook_name, workspace_path))


@app.route('/api/hooks/optional/toggle', methods=['POST'])
def api_toggle_optional_hook():
    """Toggle global enabled state of an optional hook."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    hook_name = data.get('hook_name', '')
    enabled = data.get('enabled', True)
    return jsonify(toggle_optional_hook(hook_name, enabled, manifest_manager))


@app.route('/api/hooks/optional/uninstall', methods=['POST'])
def api_uninstall_optional_hook():
    """Uninstall an optional hook from workspace (unwires from settings.json)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    hook_name = data.get('hook_name', '')
    workspace_path = manifest_manager.carl_path.parent
    return jsonify(uninstall_optional_hook(hook_name, workspace_path))


# ---- Hook Version Management ----

@app.route('/api/hooks/versions', methods=['GET'])
def api_check_hook_versions():
    """Check installed hook versions against bundled versions.

    Called when workspace loads to detect outdated hooks.
    """
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify(check_hook_versions(workspace_path))


@app.route('/api/hooks/versions/bundled', methods=['GET'])
def api_get_bundled_versions():
    """Get the bundled hook versions (what ships with this app)."""
    return jsonify({
        'success': True,
        'versions': BUNDLED_HOOK_VERSIONS
    })


@app.route('/api/hooks/update', methods=['POST'])
def api_update_hook():
    """Update a specific hook to the bundled version."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json() or {}
    hook_file = data.get('hook_file', '')
    location = data.get('location', 'workspace')

    if not hook_file:
        return jsonify({'success': False, 'error': 'hook_file is required'})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify(update_hook(hook_file, workspace_path, location))


@app.route('/api/hooks/update-all', methods=['POST'])
def api_update_all_hooks():
    """Update all outdated hooks to bundled versions."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify(update_all_hooks(workspace_path))


@app.route('/api/available-toggles', methods=['GET'])
def api_get_available_toggles():
    """Get list of available toggles for session overrides."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'error': 'No workspace active', 'toggles': []})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify({
        'success': True,
        'toggles': get_available_toggles(workspace_path)
    })


# ---- Cleanup / Uninstall API ----

@app.route('/api/cleanup/preview', methods=['GET'])
def api_cleanup_preview():
    """Preview what would be affected by cleanup operations."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    profile = database.get_active_profile()
    profile_id = profile['id'] if profile else None

    return jsonify({
        'success': True,
        'preview': get_cleanup_preview(workspace_path, profile_id)
    })


@app.route('/api/cleanup/disable', methods=['POST'])
def api_cleanup_disable():
    """Disable CARL by removing hooks from settings.json."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify(disable_carl_hooks(workspace_path))


@app.route('/api/cleanup/uninstall', methods=['POST'])
def api_cleanup_uninstall():
    """Uninstall CARL hook files (keeps .carl folder and settings.json hooks disabled)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent

    # First disable hooks in settings.json
    disable_result = disable_carl_hooks(workspace_path)

    # Then delete hook files
    uninstall_result = uninstall_carl_hooks(workspace_path)

    return jsonify({
        'success': uninstall_result.get('success', False),
        'disabled': disable_result,
        'uninstalled': uninstall_result
    })


@app.route('/api/cleanup/full', methods=['POST'])
def api_cleanup_full():
    """Full cleanup: remove .carl folder, hook files, settings.json entries, and DB records."""
    global manifest_manager

    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    profile = database.get_active_profile()
    profile_id = profile['id'] if profile else None

    result = cleanup_carl_full(workspace_path, profile_id)

    # Clear the global manifest_manager since we just deleted .carl
    manifest_manager = None

    return jsonify(result)


@app.route('/api/cleanup/addon/<hook_name>', methods=['POST'])
def api_cleanup_addon(hook_name):
    """Full cleanup of a specific addon hook."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    workspace_path = manifest_manager.carl_path.parent
    return jsonify(cleanup_addon_full(hook_name, workspace_path, manifest_manager))


# ---- Session Management API ----

@app.route('/api/sessions', methods=['GET'])
def api_get_sessions():
    """Get list of all active sessions with their configs."""
    if not session_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found', 'sessions': []})

    # Get global manifest toggles for computing effective values
    manifest_toggles = {}
    if manifest_manager:
        manifest_toggles = manifest_manager.read_toggles()

    sessions = session_manager.list_sessions(manifest_toggles)
    return jsonify({
        'success': True,
        'sessions': sessions
    })


@app.route('/api/sessions/<uuid>', methods=['GET'])
def api_get_session(uuid):
    """Get a specific session config by UUID."""
    if not session_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    manifest_toggles = {}
    if manifest_manager:
        manifest_toggles = manifest_manager.read_toggles()

    session = session_manager.get_session(uuid, manifest_toggles)
    if session:
        return jsonify({'success': True, 'session': session})
    return jsonify({'success': False, 'error': 'Session not found'})


@app.route('/api/sessions/<uuid>', methods=['PUT'])
def api_update_session(uuid):
    """Update session-specific overrides."""
    if not session_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        overrides = data.get('overrides', {})
        return jsonify(session_manager.update_session_overrides(uuid, overrides))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/sessions/<uuid>/title', methods=['PUT'])
def api_update_session_title(uuid):
    """Update session title."""
    if not session_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        title = data.get('title', '')
        return jsonify(session_manager.update_session_title(uuid, title))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/sessions/<uuid>', methods=['DELETE'])
def api_delete_session(uuid):
    """Delete a session config."""
    if not session_manager:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    return jsonify(session_manager.delete_session(uuid))


# ---- Domain Management API ----

@app.route('/api/domains', methods=['GET'])
def api_list_domains():
    """List all domains with summary info."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found', 'domains': []})

    return jsonify(domains_api.list_domains(str(manifest_manager.carl_path)))


@app.route('/api/domains/<name>', methods=['GET'])
def api_get_domain(name):
    """Get domain details including all rules."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    return jsonify(domains_api.get_domain(str(manifest_manager.carl_path), name))


@app.route('/api/domains', methods=['POST'])
def api_create_domain():
    """Create a new domain with file and manifest entries."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        name = data.get('name', '')
        config = {
            'recall': data.get('recall', ''),
            'exclude': data.get('exclude', ''),
            'always_on': data.get('always_on', False),
            'state': data.get('state', True),
            'rules': data.get('rules', [])
        }
        return jsonify(domains_api.create_domain(str(manifest_manager.carl_path), name, config))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/domains/<name>', methods=['PUT'])
def api_update_domain(name):
    """Update domain configuration (recall, exclude, state, always_on, rules)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        config = {}

        # Only include fields that were provided
        if 'recall' in data:
            config['recall'] = data['recall']
        if 'exclude' in data:
            config['exclude'] = data['exclude']
        if 'always_on' in data:
            config['always_on'] = data['always_on']
        if 'state' in data:
            config['state'] = data['state']
        if 'rules' in data:
            config['rules'] = data['rules']

        return jsonify(domains_api.update_domain(str(manifest_manager.carl_path), name, config))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/domains/<name>', methods=['DELETE'])
def api_delete_domain(name):
    """Delete a domain (file + manifest entries)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    return jsonify(domains_api.delete_domain(str(manifest_manager.carl_path), name))


# ---- Domain Rule Management API ----

@app.route('/api/domains/<name>/rules', methods=['POST'])
def api_add_domain_rule(name):
    """Add a new rule to a domain."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        rule_text = data.get('text', '')
        index = data.get('index')  # Optional - None means append

        if not rule_text:
            return jsonify({'success': False, 'error': 'Rule text is required'})

        return jsonify(domains_api.add_rule(
            str(manifest_manager.carl_path), name, rule_text, index
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/domains/<name>/rules/<int:index>', methods=['PUT'])
def api_update_domain_rule(name, index):
    """Update a specific rule in a domain."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        rule_text = data.get('text', '')

        if not rule_text:
            return jsonify({'success': False, 'error': 'Rule text is required'})

        return jsonify(domains_api.update_rule(
            str(manifest_manager.carl_path), name, index, rule_text
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/domains/<name>/rules/<int:index>', methods=['DELETE'])
def api_delete_domain_rule(name, index):
    """Delete a specific rule from a domain."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    return jsonify(domains_api.delete_rule(str(manifest_manager.carl_path), name, index))


@app.route('/api/domains/<name>/rules/reorder', methods=['POST'])
def api_reorder_domain_rules(name):
    """Reorder rules in a domain."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        new_order = data.get('order', [])

        if not isinstance(new_order, list):
            return jsonify({'success': False, 'error': 'order must be a list of indices'})

        return jsonify(domains_api.reorder_rules(
            str(manifest_manager.carl_path), name, new_order
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/domains/reorder', methods=['POST'])
def api_reorder_domains():
    """Reorder domains in the list."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        order = data.get('order', [])

        if not isinstance(order, list):
            return jsonify({'success': False, 'error': 'order must be a list of domain names'})

        return jsonify(domains_api.reorder_domains(
            str(manifest_manager.carl_path), order
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ---- Domain Import API ----

@app.route('/api/domains/import/check', methods=['POST'])
def api_check_import_source():
    """Check a folder for importable domains."""
    data = request.get_json()
    path = data.get('path', '')

    if not path:
        return jsonify({'success': False, 'error': 'No path provided'})

    source_path = Path(path)

    # Check if it's a .carl folder or contains one
    carl_path = None

    # Option 1: User selected the .carl folder directly
    if source_path.name == '.carl' and source_path.is_dir():
        carl_path = source_path
    # Option 2: User selected a workspace containing .carl
    elif (source_path / '.carl').is_dir():
        carl_path = source_path / '.carl'
    # Option 3: Check if .carl exists as sibling (e.g., user selected C:\Users\Chris and .carl is at C:\Users\Chris\.carl)
    # This handles selecting parent folder when .carl is a hidden folder at that level
    elif source_path.is_dir():
        # Look for any .env files directly in this folder (might be a .carl without the name)
        env_files = list(source_path.glob('*.env'))
        manifest_file = source_path / 'manifest.env'
        if manifest_file.exists() or len(env_files) > 0:
            # This looks like a .carl folder even if not named .carl
            carl_path = source_path

    if not carl_path:
        return jsonify({'success': False, 'error': 'No .carl folder found. Select a .carl folder or a workspace containing one.'})

    # Find all domain files (with or without .env extension)
    domains = []
    skip_names = {'manifest', 'addons', 'sessions'}  # Non-domain items (commands/context/global ARE valid domains)

    # Try to read manifest for recall keywords
    manifest_data = {}
    manifest_file = carl_path / 'manifest.env'
    if not manifest_file.exists():
        manifest_file = carl_path / 'manifest'
    if manifest_file.exists():
        try:
            for line in manifest_file.read_text(encoding='utf-8').split('\n'):
                if '=' in line and not line.strip().startswith('#'):
                    key, val = line.split('=', 1)
                    manifest_data[key.strip()] = val.strip()
        except Exception:
            pass

    # Check for all domain files (both .env and extensionless)
    env_files = list(carl_path.glob('*.env'))

    # Also check for files without extension (some .carl folders store them this way)
    seen_names = {f.stem.lower() for f in env_files}  # Track names we've seen
    for item in carl_path.iterdir():
        if item.is_file() and not item.suffix:  # File without extension
            # Only add if we don't already have a .env version of this domain
            if item.name.lower() not in seen_names:
                env_files.append(item)

    for env_file in env_files:
        # Get domain name (handle both 'domain.env' and 'domain' formats)
        domain_name = env_file.stem if env_file.suffix == '.env' else env_file.name
        domain_upper = domain_name.upper()

        if domain_name.lower() in skip_names:
            continue

        try:
            content = env_file.read_text(encoding='utf-8')
            # Check if this looks like a CARL domain file (has KEY=value format)
            # Special domains (GLOBAL, COMMANDS, CONTEXT) are always included even if empty
            is_special_domain = domain_upper in ('GLOBAL', 'COMMANDS', 'CONTEXT')
            has_key_value = any('=' in line for line in content.split('\n') if line.strip() and not line.startswith('#'))
            if not has_key_value and not is_special_domain:
                continue  # Skip files that don't look like domain files
        except Exception:
            continue

        # Handle special CORE domains differently
        if domain_upper == 'COMMANDS':
            # Parse *star commands format
            commands = {}
            rule_pattern = r'^(\w+)_RULE_(\d+)\s*=\s*(.*)$'
            for line in content.split('\n'):
                match = re.match(rule_pattern, line.strip())
                if match:
                    cmd = match.group(1).upper()
                    if cmd in ['FRESH', 'MODERATE', 'DEPLETED']:
                        continue  # Skip context brackets
                    idx = int(match.group(2))
                    text = match.group(3)
                    if cmd not in commands:
                        commands[cmd] = []
                    commands[cmd].append({'index': idx, 'text': text})

            # Sort rules by index within each command
            for c in commands:
                commands[c].sort(key=lambda r: r['index'])

            total_rules = sum(len(r) for r in commands.values())
            domains.append({
                'name': domain_name,
                'recall': '',
                'rules': [],  # Commands have nested structure
                'rule_count': total_rules,
                'is_special': True,
                'special_type': 'commands',
                'command_count': len(commands),
                'commands': {k: {'count': len(v), 'rules': [r['text'] for r in v]} for k, v in commands.items()},
                'file': str(env_file)
            })

        elif domain_upper == 'CONTEXT':
            # Parse bracket-based format (FRESH, MODERATE, DEPLETED)
            brackets = {'FRESH': [], 'MODERATE': [], 'DEPLETED': []}
            rule_pattern = r'^(FRESH|MODERATE|DEPLETED)_RULE_(\d+)\s*=\s*(.*)$'
            for line in content.split('\n'):
                match = re.match(rule_pattern, line.strip(), re.IGNORECASE)
                if match:
                    bracket = match.group(1).upper()
                    idx = int(match.group(2))
                    text = match.group(3)
                    if bracket in brackets:
                        brackets[bracket].append({'index': idx, 'text': text})

            # Sort rules by index within each bracket
            for b in brackets:
                brackets[b].sort(key=lambda r: r['index'])

            total_rules = sum(len(r) for r in brackets.values())
            domains.append({
                'name': domain_name,
                'recall': '',
                'rules': [],
                'rule_count': total_rules,
                'is_special': True,
                'special_type': 'context',
                'brackets': {k: {'count': len(v), 'rules': [r['text'] for r in v]} for k, v in brackets.items()},
                'file': str(env_file)
            })

        elif domain_upper == 'GLOBAL':
            # GLOBAL domain - standard rules, no recall (always injected)
            # Try multiple formats: GLOBAL_RULE_N=text, N=text
            rules_with_idx = []

            for line in content.split('\n'):
                line = line.strip()
                if not line or line.startswith('#'):
                    continue

                # Try GLOBAL_RULE_N=text format first
                match = re.match(r'^GLOBAL_RULE_(\d+)\s*=\s*(.*)$', line, re.IGNORECASE)
                if match:
                    rules_with_idx.append((int(match.group(1)), match.group(2)))
                    continue

                # Try simple N=text format (numeric key)
                match = re.match(r'^(\d+)\s*=\s*(.*)$', line)
                if match:
                    rules_with_idx.append((int(match.group(1)), match.group(2)))

            rules_with_idx.sort(key=lambda x: x[0])
            rules = [r[1] for r in rules_with_idx]

            domains.append({
                'name': domain_name,
                'recall': '',  # GLOBAL has no recall - always injected
                'rules': rules,
                'rule_count': len(rules),
                'is_special': True,
                'special_type': 'global',
                'file': str(env_file)
            })

        else:
            # Standard domain format
            rules = []
            recall = ''
            rule_pattern = rf'^{domain_upper}_RULE_(\d+)=(.*)$'

            rules_with_idx = []
            for line in content.split('\n'):
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                match = re.match(rule_pattern, line, re.IGNORECASE)
                if match:
                    rules_with_idx.append((int(match.group(1)), match.group(2)))
                elif '=' in line:
                    key, val = line.split('=', 1)
                    if key.strip().lower() == 'recall':
                        recall = val.strip()

            rules_with_idx.sort(key=lambda x: x[0])
            rules = [r[1] for r in rules_with_idx]

            # Check manifest for recall if not in domain file
            if not recall:
                recall = manifest_data.get(f'{domain_name}_RECALL', '') or manifest_data.get(f'{domain_upper}_RECALL', '')

            domains.append({
                'name': domain_name,
                'recall': recall,
                'rules': rules,
                'rule_count': len(rules),
                'file': str(env_file)
            })

    return jsonify({
        'success': True,
        'carl_path': str(carl_path),
        'domains': domains
    })


@app.route('/api/domains/import', methods=['POST'])
def api_import_domains():
    """Import selected domains from another .carl folder."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    data = request.get_json()
    source_path = data.get('source_path', '')
    domain_names = data.get('domains', [])

    if not source_path:
        return jsonify({'success': False, 'error': 'No source path provided'})
    if not domain_names:
        return jsonify({'success': False, 'error': 'No domains selected'})

    source_carl = Path(source_path)
    target_carl = manifest_manager.carl_path

    if not source_carl.exists():
        return jsonify({'success': False, 'error': 'Source .carl folder not found'})

    # Check if source and target are the same (resolving symlinks)
    if source_carl.resolve() == target_carl.resolve():
        return jsonify({'success': False, 'error': 'Cannot import from the same .carl folder you are managing'})

    # Read source manifest for recall keywords
    source_manifest = {}
    manifest_file = source_carl / 'manifest.env'
    if not manifest_file.exists():
        manifest_file = source_carl / 'manifest'
    if manifest_file.exists():
        try:
            for line in manifest_file.read_text(encoding='utf-8').split('\n'):
                if '=' in line and not line.strip().startswith('#'):
                    key, val = line.split('=', 1)
                    source_manifest[key.strip()] = val.strip()
        except Exception:
            pass

    imported = []
    errors = []

    for name in domain_names:
        # Try both with and without .env extension
        source_file = source_carl / f'{name}.env'
        if not source_file.exists():
            source_file = source_carl / name  # Try without extension
        if not source_file.exists():
            errors.append(f'{name}: file not found')
            continue

        try:
            # Check for existing target file (prefer .env, fallback to extensionless)
            target_file = target_carl / f'{name}.env'
            if not target_file.exists():
                extensionless = target_carl / name
                if extensionless.exists():
                    target_file = extensionless  # Use existing extensionless file
            domain_upper = name.upper()

            # Read source rules
            source_content = source_file.read_text(encoding='utf-8')
            source_lines = source_content.strip().split('\n') if source_content.strip() else []

            # Parse source rules (extract key=value pairs, skip comments)
            source_rules = []
            for line in source_lines:
                line = line.strip()
                if line and not line.startswith('#') and '=' in line:
                    key, val = line.split('=', 1)
                    source_rules.append((key.strip(), val.strip()))

            if target_file.exists():
                # APPEND MODE: Target exists, merge rules
                target_content = target_file.read_text(encoding='utf-8')
                target_lines = target_content.strip().split('\n') if target_content.strip() else []

                # Parse existing rules and their values
                existing_rules = {}  # key -> value
                existing_values = set()  # for duplicate detection by value
                max_indices = {}  # prefix -> max index (e.g., 'GLOBAL_RULE' -> 5)

                for line in target_lines:
                    line = line.strip()
                    if line and not line.startswith('#') and '=' in line:
                        key, val = line.split('=', 1)
                        key = key.strip()
                        val = val.strip()
                        existing_rules[key] = val
                        existing_values.add(val)

                        # Track max index for each rule prefix (e.g., GLOBAL_RULE_0, FRESH_RULE_2)
                        match = re.match(r'^(.+_RULE)_(\d+)$', key)
                        if match:
                            prefix, idx = match.groups()
                            max_indices[prefix] = max(max_indices.get(prefix, -1), int(idx))

                # Append non-duplicate source rules
                new_lines = []
                for src_key, src_val in source_rules:
                    # Skip if exact value already exists (duplicate detection)
                    if src_val in existing_values:
                        continue

                    # Determine the rule prefix and renumber
                    match = re.match(r'^(.+_RULE)_(\d+)$', src_key)
                    if match:
                        prefix = match.group(1)
                        next_idx = max_indices.get(prefix, -1) + 1
                        max_indices[prefix] = next_idx
                        new_key = f'{prefix}_{next_idx}'
                        new_lines.append(f'{new_key}={src_val}')
                        existing_values.add(src_val)  # Track for subsequent duplicates
                    else:
                        # Non-rule line (like RECALL), only add if not exists
                        if src_key not in existing_rules:
                            new_lines.append(f'{src_key}={src_val}')

                # Append new lines to file
                if new_lines:
                    with open(target_file, 'a', encoding='utf-8') as f:
                        f.write('\n' + '\n'.join(new_lines))
            else:
                # NEW FILE: Just copy the source
                shutil.copy2(source_file, target_file)

            # Register in manifest
            manifest_manager.write_toggle(f'{domain_upper}_STATE', True)

            # Copy recall from source manifest (merge keywords if existing)
            source_recall = source_manifest.get(f'{name}_RECALL', '') or source_manifest.get(f'{domain_upper}_RECALL', '')
            if source_recall:
                existing_recall = manifest_manager.get_value(f'{domain_upper}_RECALL') or ''
                if existing_recall:
                    # Merge recall keywords, avoiding duplicates
                    existing_keywords = set(k.strip() for k in existing_recall.split(',') if k.strip())
                    new_keywords = [k.strip() for k in source_recall.split(',') if k.strip()]
                    for kw in new_keywords:
                        if kw not in existing_keywords:
                            existing_keywords.add(kw)
                    merged_recall = ','.join(sorted(existing_keywords))
                    manifest_manager.set_value(f'{domain_upper}_RECALL', merged_recall)
                else:
                    manifest_manager.set_value(f'{domain_upper}_RECALL', source_recall)

            imported.append(name)
        except Exception as e:
            errors.append(f'{name}: {str(e)}')

    return jsonify({
        'success': len(errors) == 0 or len(imported) > 0,
        'imported': imported,
        'errors': errors
    })


# ---- Special Domain Routes (CONTEXT & COMMANDS) ----

@app.route('/api/context/brackets/<bracket>', methods=['PUT'])
def api_update_context_bracket(bracket):
    """Update a CONTEXT bracket."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        return jsonify(domains_api.update_context_bracket(
            str(manifest_manager.carl_path), bracket, data
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/commands/<command>', methods=['PUT'])
def api_update_command(command):
    """Update a star command."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        return jsonify(domains_api.update_command(
            str(manifest_manager.carl_path), command, data
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/commands/<command>', methods=['DELETE'])
def api_delete_command(command):
    """Delete a star command."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        return jsonify(domains_api.delete_command(
            str(manifest_manager.carl_path), command
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/commands/reorder', methods=['POST'])
def api_reorder_commands():
    """Reorder star commands."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No CARL installation found'})

    try:
        data = request.get_json()
        order = data.get('order', [])
        return jsonify(domains_api.reorder_commands(
            str(manifest_manager.carl_path), order
        ))
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ---- Resources API ----

@app.route('/api/resources', methods=['GET'])
def get_resources():
    """Get list of available resources (hooks, skills, commands)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active', 'resources': []})

    return jsonify(resources_api.discover_resources(str(manifest_manager.carl_path)))


@app.route('/api/resources/install', methods=['POST'])
def install_resource():
    """Install a resource (copy files to targets)."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    resource_id = data.get('resource_id')
    target = data.get('target', 'global')  # 'global' or 'project'

    if not resource_id:
        return jsonify({'success': False, 'error': 'No resource_id provided'})

    # Determine install path based on target
    if target == 'project':
        # Install to workspace .claude folder
        claude_path = manifest_manager.carl_path.parent / '.claude'
    else:
        # Install to global ~/.claude folder
        claude_path = Path.home() / '.claude'

    return jsonify(resources_api.install_resource(
        str(manifest_manager.carl_path),
        resource_id,
        claude_path=str(claude_path)
    ))


@app.route('/api/resources/wire', methods=['POST'])
def wire_resource():
    """Wire a resource's hooks to settings.json."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    resource_id = data.get('resource_id')

    if not resource_id:
        return jsonify({'success': False, 'error': 'No resource_id provided'})

    return jsonify(resources_api.wire_resource(str(manifest_manager.carl_path), resource_id))


@app.route('/api/resources/toggle', methods=['POST'])
def toggle_resource():
    """Toggle a resource's global enabled state."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    resource_id = data.get('resource_id')
    enabled = data.get('enabled', True)

    if not resource_id:
        return jsonify({'success': False, 'error': 'No resource_id provided'})

    return jsonify(resources_api.toggle_resource(str(manifest_manager.carl_path), resource_id, enabled))


@app.route('/api/resources/uninstall', methods=['POST'])
def uninstall_resource():
    """Uninstall a resource."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    resource_id = data.get('resource_id')
    delete_files = data.get('delete_files', False)

    if not resource_id:
        return jsonify({'success': False, 'error': 'No resource_id provided'})

    return jsonify(resources_api.uninstall_resource(str(manifest_manager.carl_path), resource_id, delete_files))


@app.route('/api/resources/snippet', methods=['GET'])
def get_resource_snippet():
    """Get settings.json snippet for a resource."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active'})

    resource_id = request.args.get('resource_id')

    if not resource_id:
        return jsonify({'success': False, 'error': 'No resource_id provided'})

    return jsonify(resources_api.get_resource_snippet(str(manifest_manager.carl_path), resource_id))


@app.route('/api/resources/toggles', methods=['GET'])
def get_resource_toggles():
    """Get installed hook resources for Settings tab toggles."""
    if not manifest_manager or not manifest_manager.carl_path:
        return jsonify({'success': False, 'error': 'No workspace active', 'resources': []})

    return jsonify({
        'success': True,
        'resources': resources_api.get_installed_hook_resources(str(manifest_manager.carl_path))
    })


# ---- Addons API ----

@app.route('/api/addons', methods=['GET'])
def get_addons():
    """Get list of available and installed addons."""
    if addons_manager:
        return jsonify(addons_manager.discover_addons())

    # No workspace - still return bundled addons so users can see what's available
    bundled_addons_path = BASE_DIR / 'static' / 'addons'
    available = []
    if bundled_addons_path.exists():
        import json as json_mod
        for addon_file in bundled_addons_path.glob('*.json'):
            try:
                content = json_mod.loads(addon_file.read_text())
                # Skip disabled addons (enabled defaults to true if not specified)
                if content.get('type') == 'carl-addon' and content.get('enabled') is not False:
                    metadata = content.get('metadata', {})
                    domains = content.get('domains', [])
                    special = content.get('special_domains', {})
                    available.append({
                        'name': metadata.get('name', addon_file.stem),
                        'description': metadata.get('description', ''),
                        'author': metadata.get('author', 'Unknown'),
                        'domain_count': len(domains) + len(special.get('COMMANDS', {}).get('commands', {})),
                        'tags': metadata.get('tags', []),
                        'source': 'bundled',
                        'path': str(addon_file),
                        'installed': False
                    })
            except Exception:
                pass
    return jsonify({'success': True, 'available': available, 'installed': []})


@app.route('/api/addons/preview', methods=['POST'])
def preview_addon():
    """Preview an addon before installation."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    addon_path = data.get('path')
    if not addon_path:
        return jsonify({'success': False, 'error': 'No addon path provided'})

    return jsonify(addons_manager.preview_addon(addon_path))


@app.route('/api/addons/install', methods=['POST'])
def install_addon():
    """Install an addon to the workspace."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    addon_path = data.get('path')
    options = data.get('options', {})

    if not addon_path:
        return jsonify({'success': False, 'error': 'No addon path provided'})

    return jsonify(addons_manager.install_addon(addon_path, options))


@app.route('/api/addons/sync-commands', methods=['POST'])
def sync_commands_addon():
    """Sync COMMANDS addon - add checked commands, remove unchecked."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    addon_path = data.get('path')
    selected_commands = data.get('selected_commands', [])

    if not addon_path:
        return jsonify({'success': False, 'error': 'No addon path provided'})

    return jsonify(addons_manager.sync_commands_addon(addon_path, selected_commands))


@app.route('/api/addons/<name>/preview-uninstall', methods=['GET'])
def preview_uninstall_addon(name):
    """Preview what would be uninstalled for an addon."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    return jsonify(addons_manager.preview_uninstall(name))


@app.route('/api/addons/<name>', methods=['DELETE'])
def uninstall_addon(name):
    """Uninstall an addon from the workspace."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json() or {}
    command_action = data.get('command_action', 'remove_all')
    commands_to_remove = data.get('commands_to_remove', None)

    # Debug logging
    print(f"[UNINSTALL DEBUG] Addon: {name}")
    print(f"[UNINSTALL DEBUG] Raw data: {data}")
    print(f"[UNINSTALL DEBUG] command_action: {command_action}")
    print(f"[UNINSTALL DEBUG] commands_to_remove: {commands_to_remove}")

    return jsonify(addons_manager.uninstall_addon(name, command_action, commands_to_remove))


@app.route('/api/addons/exportable', methods=['GET'])
def get_exportable_domains():
    """Get list of domains that can be exported."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active', 'domains': []})

    return jsonify(addons_manager.get_exportable_domains())


@app.route('/api/addons/export', methods=['POST'])
def export_addon():
    """Export selected domains as an addon."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    domain_names = data.get('domains', [])
    options = data.get('options', {})

    if not domain_names:
        return jsonify({'success': False, 'error': 'No domains selected'})

    return jsonify(addons_manager.export_addon(domain_names, options))


@app.route('/api/addons/save', methods=['POST'])
def save_addon():
    """Save exported addon JSON to the available directory."""
    if not addons_manager:
        return jsonify({'success': False, 'error': 'No workspace active'})

    data = request.get_json()
    content = data.get('content')
    filename = data.get('filename')

    if not content or not filename:
        return jsonify({'success': False, 'error': 'Missing content or filename'})

    return jsonify(addons_manager.save_addon_to_available(content, filename))


# ---- Update API ----

@app.route('/api/update/version', methods=['GET'])
def api_get_version():
    """Get current app version."""
    return jsonify({
        'version': get_version()
    })


@app.route('/api/update/status', methods=['GET'])
def api_get_update_status():
    """Get current update status."""
    return jsonify(get_update_status())


@app.route('/api/update/check', methods=['POST'])
def api_check_for_updates():
    """Check for updates (synchronous)."""
    data = request.get_json(silent=True) or {}
    force = data.get('force', False)
    return jsonify(check_for_updates(force=force))


@app.route('/api/update/download', methods=['POST'])
def api_download_update():
    """Start downloading the update."""
    status = get_update_status()

    if not status['available']:
        return jsonify({'success': False, 'error': 'No update available'})

    if status['downloading']:
        return jsonify({'success': False, 'error': 'Download already in progress'})

    # Start async download
    download_update_async()
    return jsonify({'success': True, 'message': 'Download started'})


@app.route('/api/update/apply', methods=['POST'])
def api_apply_update():
    """Apply the downloaded update (triggers restart)."""
    result = apply_update()

    if result.get('action') == 'exit_app':
        # Schedule app exit after response is sent
        import threading

        def delayed_exit():
            import time
            time.sleep(1)
            # Signal the app to quit
            import os
            os._exit(0)

        threading.Thread(target=delayed_exit, daemon=True).start()

    return jsonify(result)


@app.route('/api/update/dismiss', methods=['POST'])
def api_dismiss_update():
    """Dismiss update notification."""
    reset_update_state()
    return jsonify({'success': True})


# ---- Startup ----

if __name__ == '__main__':
    init_database()
    load_active_profile()
    app.run(host='127.0.0.1', port=5050, debug=False)
