"""
Addons system for CARL Control Panel.
Handles discovering, previewing, installing, and exporting domain packs.
"""
import json
import shutil
from datetime import datetime
from pathlib import Path
from typing import Optional


# Addon schema version
ADDON_SCHEMA_VERSION = "1.0"


class AddonsManager:
    """Manages addon discovery, installation, and export."""

    def __init__(self, carl_path: Path, bundled_addons_path: Optional[Path] = None):
        """Initialize with paths to CARL directory and bundled addons.

        Args:
            carl_path: Path to .carl directory
            bundled_addons_path: Path to bundled addons (static/addons/)
        """
        self.carl_path = Path(carl_path)
        self.workspace_path = self.carl_path.parent
        self.addons_path = self.carl_path / 'addons'
        self.available_path = self.addons_path / 'available'
        self.installed_registry = self.addons_path / 'installed.json'
        self.bundled_addons_path = bundled_addons_path

    def _ensure_addons_dir(self):
        """Create addons directories if they don't exist."""
        self.addons_path.mkdir(parents=True, exist_ok=True)
        self.available_path.mkdir(parents=True, exist_ok=True)

    def _load_installed_registry(self) -> dict:
        """Load the installed addons registry."""
        if not self.installed_registry.exists():
            return {'addons': []}
        try:
            return json.loads(self.installed_registry.read_text())
        except Exception:
            return {'addons': []}

    def _save_installed_registry(self, registry: dict):
        """Save the installed addons registry."""
        self._ensure_addons_dir()
        self.installed_registry.write_text(json.dumps(registry, indent=2))

    def discover_addons(self) -> dict:
        """Discover available addons from available/ directory and bundled addons.

        Returns:
            Dict with 'available' and 'installed' lists.
        """
        self._ensure_addons_dir()
        available = []
        installed_registry = self._load_installed_registry()
        installed = installed_registry.get('addons', [])

        # Scan available directory
        if self.available_path.exists():
            for addon_file in self.available_path.glob('*.json'):
                addon_info = self._read_addon_info(addon_file)
                if addon_info:
                    addon_info['source'] = 'available'
                    addon_info['path'] = str(addon_file)
                    available.append(addon_info)

        # Scan bundled addons
        if self.bundled_addons_path and self.bundled_addons_path.exists():
            for addon_file in self.bundled_addons_path.glob('*.json'):
                addon_info = self._read_addon_info(addon_file)
                if addon_info:
                    addon_info['source'] = 'bundled'
                    addon_info['path'] = str(addon_file)
                    # Check if already in available list (name collision)
                    if not any(a['name'] == addon_info['name'] for a in available):
                        available.append(addon_info)

        # Mark installed status
        installed_names = {a.get('name') for a in installed}
        for addon in available:
            addon['installed'] = addon['name'] in installed_names

        return {
            'success': True,
            'available': available,
            'installed': installed
        }

    def _read_addon_info(self, addon_path: Path) -> Optional[dict]:
        """Read basic info from an addon file without full parsing.

        Returns:
            Dict with name, description, author, domain_count, tags, or None if invalid.
        """
        try:
            content = json.loads(addon_path.read_text())

            # Validate basic structure
            if content.get('type') != 'carl-addon':
                return None

            # Skip disabled addons (enabled defaults to true if not specified)
            if content.get('enabled') is False:
                return None

            metadata = content.get('metadata', {})
            domains = content.get('domains', [])
            special_domains = content.get('special_domains', {})

            domain_count = len(domains)
            if special_domains:
                domain_count += len([k for k in special_domains if special_domains[k]])

            return {
                'name': metadata.get('name', addon_path.stem),
                'description': metadata.get('description', ''),
                'author': metadata.get('author', ''),
                'tags': metadata.get('tags', []),
                'domain_count': domain_count,
                'created': metadata.get('created', ''),
                'version': content.get('version', ADDON_SCHEMA_VERSION)
            }
        except Exception:
            return None

    def preview_addon(self, addon_path: str) -> dict:
        """Parse and validate addon, return detailed preview data.

        Args:
            addon_path: Path to addon JSON file.

        Returns:
            Dict with addon details and conflict info.
        """
        try:
            path = Path(addon_path)
            if not path.exists():
                return {'success': False, 'error': f'Addon file not found: {addon_path}'}

            content = json.loads(path.read_text())

            # Validate schema
            if content.get('type') != 'carl-addon':
                return {'success': False, 'error': 'Invalid addon format: missing type'}

            metadata = content.get('metadata', {})
            addon_name = metadata.get('name', path.stem)
            domains = content.get('domains', [])
            special_domains = content.get('special_domains', {})

            # Check if this addon is installed (to differentiate "installed by us" vs "conflict")
            registry = self._load_installed_registry()
            installed_by_this_addon = set()
            for addon in registry.get('addons', []):
                if addon.get('name') == addon_name:
                    # This addon is installed - get its domains
                    installed_by_this_addon = set(addon.get('domains', []))
                    break

            # Check for conflicts with existing domains
            conflicts = []
            installed_domains = []
            for domain in domains:
                domain_name = domain.get('name', '').upper()
                domain_file = self.carl_path / domain_name.lower()
                if domain_file.exists():
                    if domain_name in installed_by_this_addon:
                        # Installed by this addon - not a conflict
                        installed_domains.append(domain_name)
                    else:
                        # Exists but installed by something else
                        conflicts.append({
                            'domain': domain['name'],
                            'type': 'domain',
                            'exists': True
                        })

            # Check special domains
            for special_name in special_domains:
                special_file = self.carl_path / special_name.lower()
                if special_file.exists() and special_name not in installed_by_this_addon:
                    conflicts.append({
                        'domain': special_name,
                        'type': 'special',
                        'exists': True
                    })

            # Build special domains preview - handle COMMANDS specially
            special_domains_preview = []

            for name, data in special_domains.items():
                if not data:
                    continue

                # COMMANDS with hierarchical structure (commands with rules)
                if name == 'COMMANDS' and 'commands' in data:
                    commands_preview = self._preview_commands_addon(
                        addon_name, data['commands']
                    )
                    special_domains_preview.append(commands_preview)
                # CONTEXT with bracket structure (FRESH/MODERATE/DEPLETED)
                elif name == 'CONTEXT' and 'brackets' in data:
                    brackets_data = data['brackets']
                    bracket_counts = {}
                    total_rules = 0
                    for bracket_name, bracket_info in brackets_data.items():
                        rules = bracket_info.get('rules', []) if isinstance(bracket_info, dict) else []
                        bracket_counts[bracket_name] = len(rules)
                        total_rules += len(rules)
                    special_domains_preview.append({
                        'name': name,
                        'is_context_addon': True,
                        'rule_count': total_rules,
                        'brackets': bracket_counts,
                        'brackets_data': brackets_data,
                        'has_conflict': any(c['domain'] == name for c in conflicts)
                    })
                else:
                    # Regular special domain with flat rules
                    special_domains_preview.append({
                        'name': name,
                        'rule_count': len(data.get('rules', [])) if data else 0,
                        'rules': data.get('rules', []) if data else [],
                        'has_conflict': any(c['domain'] == name for c in conflicts)
                    })

            return {
                'success': True,
                'metadata': metadata,
                'addon_subtype': content.get('addon_subtype'),  # 'commands' for command addons
                'is_installed': len(installed_by_this_addon) > 0,
                'domains': [
                    {
                        'name': d.get('name'),
                        'rule_count': len(d.get('rules', [])),
                        'rules': d.get('rules', []),
                        'state': d.get('state', 'inactive'),
                        'always_on': d.get('always_on', False),
                        'recall': d.get('recall', ''),
                        'has_conflict': any(c['domain'] == d.get('name') for c in conflicts),
                        'is_installed': d.get('name', '').upper() in installed_domains
                    }
                    for d in domains
                ],
                'special_domains': special_domains_preview,
                'conflicts': conflicts,
                'has_conflicts': len(conflicts) > 0,
                'installed_domains': installed_domains,
                'manifest_settings': content.get('manifest_settings', {}),
                'optional_hooks': content.get('optional_hooks', [])
            }

        except json.JSONDecodeError as e:
            return {'success': False, 'error': f'Invalid JSON: {e}'}
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _preview_commands_addon(self, addon_name: str, pack_commands: dict) -> dict:
        """Build preview data for COMMANDS addon with ownership/conflict info.

        Args:
            addon_name: Name of this addon pack.
            pack_commands: Dict of {command_name: {rules: [...]}} from the addon.

        Returns:
            Preview dict with command-level ownership and conflict info.
        """
        from .domains import DomainManager

        # Get current COMMANDS state
        domains_manager = DomainManager(self.carl_path)
        commands_file = self.carl_path / 'commands'
        current_commands = {}
        if commands_file.exists():
            current_commands = domains_manager._parse_commands(commands_file)

        # Get registry to find ownership
        registry = self._load_installed_registry()
        # Track ownership by installed name AND by original name
        ownership_by_installed = {}  # {installed_name: addon_name}
        ownership_by_original = {}   # {original_name: (addon_name, installed_name)}

        for addon in registry.get('addons', []):
            if addon.get('commands_owned'):
                addon_name_reg = addon.get('name', 'Unknown')
                for installed_name, original_name in addon['commands_owned'].items():
                    ownership_by_installed[installed_name] = addon_name_reg
                    ownership_by_original[original_name] = (addon_name_reg, installed_name)

        # Build command previews
        commands_preview = []
        for cmd_name, cmd_data in pack_commands.items():
            # Skip metadata keys like _order
            if cmd_name.startswith('_'):
                continue
            cmd_upper = cmd_name.upper()

            # Check if this addon owns this command (by original name)
            owned_info = ownership_by_original.get(cmd_upper)
            owned_by_this_addon = owned_info and owned_info[0] == addon_name
            installed_name = owned_info[1] if owned_info else cmd_upper

            # Check if the installed name exists in domain
            exists_in_domain = installed_name in current_commands and installed_name != '_order'

            # Also check if original name exists (in case not renamed)
            if not exists_in_domain and cmd_upper in current_commands and cmd_upper != '_order':
                exists_in_domain = True
                installed_name = cmd_upper

            # Determine state
            if owned_by_this_addon and exists_in_domain:
                # This pack owns it and it's installed
                state = 'installed'
                source_pack = addon_name
            elif exists_in_domain:
                # Exists but owned by someone else or not tracked
                owner = ownership_by_installed.get(installed_name) or ownership_by_installed.get(cmd_upper)
                if owner:
                    state = 'conflict'
                    source_pack = owner
                else:
                    state = 'conflict'
                    source_pack = 'existing'
            else:
                # Doesn't exist - available to install
                state = 'available'
                source_pack = None

            commands_preview.append({
                'name': cmd_upper,
                'rule_count': len(cmd_data.get('rules', [])),
                'rules': cmd_data.get('rules', []),
                'state': state,  # 'installed', 'conflict', 'available'
                'source_pack': source_pack,  # Which pack owns it (if any)
                'checked': state in ['installed', 'conflict']  # Pre-check if exists
            })

        return {
            'name': 'COMMANDS',
            'is_commands_addon': True,
            'commands': commands_preview,
            'has_conflict': any(c['state'] == 'conflict' for c in commands_preview)
        }

    def install_addon(self, addon_path: str, options: Optional[dict] = None) -> dict:
        """Install addon domains and update manifest.

        Args:
            addon_path: Path to addon JSON file.
            options: Installation options:
                - selected_domains: List of domain names to install (default: all)
                - selected_rules: Dict of {domain: [rule_indices]} for rule-level selection
                - overwrite: Whether to overwrite existing domains (default: False)
                - merge: Whether to merge rules into existing domains (default: False)
                - activate: Whether to activate domains (default: False, domains start inactive)

        Returns:
            Dict with installation results.
        """
        options = options or {}
        selected_domains = options.get('selected_domains')
        selected_rules = options.get('selected_rules', {})  # {domain: [indices]}
        overwrite = options.get('overwrite', False)
        merge = options.get('merge', False)
        activate = options.get('activate', False)

        try:
            path = Path(addon_path)
            if not path.exists():
                return {'success': False, 'error': f'Addon file not found: {addon_path}'}

            content = json.loads(path.read_text())
            metadata = content.get('metadata', {})
            addon_name = metadata.get('name', path.stem)
            domains = content.get('domains', [])
            special_domains = content.get('special_domains', {})

            installed_domains = []
            merged_domains = []
            skipped_domains = []
            errors = []

            # Helper to filter rules by selection
            def filter_rules(rules, domain_name):
                if domain_name in selected_rules:
                    selected_indices = set(selected_rules[domain_name])
                    return [r for r in rules if r.get('index') in selected_indices]
                return rules

            # Helper to install or merge a domain
            def install_domain(domain_name, rules, is_special=False, domain_config=None):
                domain_file = self.carl_path / domain_name.lower()

                # Filter rules by selection
                rules_to_install = filter_rules(rules, domain_name)
                if not rules_to_install:
                    return  # No rules selected for this domain

                # Handle existing domain
                if domain_file.exists():
                    if merge:
                        # Merge mode: append selected rules to existing
                        try:
                            merged_count = self._merge_rules_into_domain(
                                domain_file, domain_name, rules_to_install, addon_name
                            )
                            if merged_count > 0:
                                merged_domains.append({
                                    'name': domain_name,
                                    'rules_added': merged_count
                                })
                        except Exception as e:
                            errors.append(f"{domain_name} (merge): {e}")
                        return
                    elif not overwrite:
                        skipped_domains.append({
                            'name': domain_name,
                            'reason': 'exists'
                        })
                        return

                # Write domain file (new or overwrite)
                try:
                    lines = [f"# {domain_name} Domain Rules"]
                    lines.append(f"# Imported from addon: {addon_name}")
                    lines.append("")

                    for i, rule in enumerate(rules_to_install):
                        text = rule.get('text', '')
                        lines.append(f"{domain_name}_RULE_{i}={text}")

                    domain_file.write_text('\n'.join(lines))

                    # Update manifest for non-special domains
                    if not is_special and domain_config:
                        self._update_manifest_for_domain(
                            domain_name,
                            state='active' if activate else 'inactive',
                            always_on=domain_config.get('always_on', False),
                            recall=domain_config.get('recall', ''),
                            exclude=domain_config.get('exclude', '')
                        )

                    installed_domains.append(domain_name)
                except Exception as e:
                    errors.append(f"{domain_name}: {e}")

            # Install regular domains (skip COMMANDS - handled in special_domains)
            for domain in domains:
                domain_name = domain.get('name')
                if not domain_name:
                    continue

                # Skip COMMANDS - must be handled via special_domains path to preserve format
                if domain_name.upper() == 'COMMANDS':
                    errors.append("COMMANDS: Must use special_domains.COMMANDS.commands format, not regular domains array")
                    continue

                # Check if selected (domain-level or has rule selections)
                has_domain_selected = selected_domains and domain_name in selected_domains
                has_rules_selected = domain_name in selected_rules and len(selected_rules[domain_name]) > 0

                if selected_domains and not has_domain_selected and not has_rules_selected:
                    continue

                install_domain(
                    domain_name,
                    domain.get('rules', []),
                    is_special=False,
                    domain_config=domain
                )

            # Install special domains (CONTEXT, COMMANDS)
            selected_commands = options.get('selected_commands', [])

            for special_name, special_data in special_domains.items():
                if not special_data:
                    continue

                # COMMANDS with hierarchical structure (commands with rules)
                if special_name == 'COMMANDS' and 'commands' in special_data:
                    commands_to_install = special_data['commands']

                    # Filter by selection if provided
                    if selected_commands:
                        commands_to_install = {
                            k: v for k, v in commands_to_install.items()
                            if k in selected_commands
                        }

                    if commands_to_install:
                        try:
                            added = self._merge_commands(commands_to_install, addon_name)
                            if added:
                                merged_domains.append({
                                    'name': 'COMMANDS',
                                    'rules_added': len(added),
                                    'commands_added': added
                                })
                        except Exception as e:
                            errors.append(f"COMMANDS: {e}")
                    continue

                # Regular special domain with flat rules
                has_domain_selected = selected_domains and special_name in selected_domains
                has_rules_selected = special_name in selected_rules and len(selected_rules[special_name]) > 0

                if selected_domains and not has_domain_selected and not has_rules_selected:
                    continue

                install_domain(
                    special_name,
                    special_data.get('rules', []),
                    is_special=True
                )

            # Register in installed registry
            if installed_domains or merged_domains:
                registry = self._load_installed_registry()

                # Build registry entry
                entry = {
                    'name': addon_name,
                    'source_file': str(path),
                    'domains': installed_domains,
                    'merged': [m['name'] for m in merged_domains],
                    'installed_at': datetime.now().isoformat()
                }

                # Add commands_added for COMMANDS merges
                for m in merged_domains:
                    if m.get('commands_added'):
                        entry['commands_added'] = m['commands_added']
                        break

                registry['addons'].append(entry)
                self._save_installed_registry(registry)

            return {
                'success': len(errors) == 0,
                'addon_name': addon_name,
                'installed': installed_domains,
                'merged': merged_domains,
                'skipped': skipped_domains,
                'errors': errors
            }

        except json.JSONDecodeError as e:
            return {'success': False, 'error': f'Invalid JSON: {e}'}
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _merge_commands(self, commands: dict, addon_name: str) -> list:
        """Merge commands into existing COMMANDS domain.

        Args:
            commands: Dict of {command_name: {rules: [...]}}
            addon_name: Name of addon (for comments).

        Returns:
            List of command names added.
        """
        from .domains import DomainManager

        domains_manager = DomainManager(self.carl_path)
        commands_file = self.carl_path / 'commands'

        # Get existing commands for duplicate detection
        existing_commands = {}
        if commands_file.exists():
            existing_commands = domains_manager._parse_commands(commands_file)

        added_commands = []

        for cmd_name, cmd_data in commands.items():
            rules = cmd_data.get('rules', [])
            if not rules:
                continue

            # Check for exact duplicate (same command name with same rules)
            cmd_upper = cmd_name.upper()
            if cmd_upper in existing_commands:
                existing_rules = existing_commands[cmd_upper].get('rules', [])
                # Compare rule texts
                new_texts = [r.get('text', '') for r in rules]
                existing_texts = [r.get('text', '') for r in existing_rules]
                if new_texts == existing_texts:
                    # Exact duplicate - skip
                    continue

            # Use domains API to update/create command
            result = domains_manager.update_command(cmd_name, {'rules': rules})
            if result.get('success'):
                added_commands.append(cmd_name)

        return added_commands

    def sync_commands_addon(self, addon_path: str, selected_commands: list) -> dict:
        """Sync COMMANDS addon state - add checked, remove unchecked.

        This is the "settings panel" approach for COMMANDS addons:
        - Checked commands get added (with conflict renaming if needed)
        - Unchecked commands that were from this pack get removed

        Args:
            addon_path: Path to addon JSON file.
            selected_commands: List of command names user wants active.

        Returns:
            Dict with sync results including any renames.
        """
        from .domains import DomainManager

        try:
            path = Path(addon_path)
            if not path.exists():
                return {'success': False, 'error': f'Addon file not found: {addon_path}'}

            content = json.loads(path.read_text())
            metadata = content.get('metadata', {})
            addon_name = metadata.get('name', path.stem)
            special_domains = content.get('special_domains', {})

            # Get pack commands
            if 'COMMANDS' not in special_domains or 'commands' not in special_domains['COMMANDS']:
                return {'success': False, 'error': 'Not a COMMANDS addon'}

            pack_commands = special_domains['COMMANDS']['commands']
            domains_manager = DomainManager(self.carl_path)
            commands_file = self.carl_path / 'commands'

            # Get current COMMANDS state
            current_commands = {}
            if commands_file.exists():
                current_commands = domains_manager._parse_commands(commands_file)

            # Get registry and current ownership
            registry = self._load_installed_registry()
            command_ownership = {}  # {command_name: addon_name}
            addon_entry = None
            addon_entry_index = -1

            for i, addon in enumerate(registry.get('addons', [])):
                if addon.get('commands_owned'):
                    for cmd_name in addon['commands_owned']:
                        command_ownership[cmd_name] = addon.get('name')
                if addon.get('name') == addon_name:
                    addon_entry = addon
                    addon_entry_index = i

            # Commands this pack currently owns (new format: {installed_name: original_name})
            owned_by_this_pack = {}
            if addon_entry and addon_entry.get('commands_owned'):
                owned_by_this_pack = addon_entry['commands_owned'].copy()

            # Also handle legacy format: commands_added was just a list of names
            legacy_commands = set()
            if addon_entry and addon_entry.get('commands_added'):
                for cmd in addon_entry['commands_added']:
                    cmd_upper = cmd.upper()
                    # Add to owned_by_this_pack if not already tracked
                    # In legacy format, installed name = original name
                    if cmd_upper not in owned_by_this_pack:
                        # Only add if it exists in current domain
                        if cmd_upper in current_commands:
                            owned_by_this_pack[cmd_upper] = cmd_upper
                            legacy_commands.add(cmd_upper)

            # Normalize selected commands to uppercase
            selected_upper = {c.upper() for c in selected_commands}

            # Track changes
            added = []
            removed = []
            renamed = []
            errors = []

            # Process each command in the pack
            for cmd_name, cmd_data in pack_commands.items():
                cmd_upper = cmd_name.upper()
                is_selected = cmd_upper in selected_upper
                # Check if we own this command (by original name in values)
                is_owned = cmd_upper in owned_by_this_pack.values()

                if is_selected and not is_owned:
                    # Need to add this command
                    final_name = self._get_available_command_name(
                        cmd_upper, current_commands, command_ownership, addon_name
                    )

                    rules = cmd_data.get('rules', [])
                    if rules:
                        result = domains_manager.update_command(final_name, {'rules': rules})
                        if result.get('success'):
                            added.append({
                                'original': cmd_upper,
                                'installed_as': final_name,
                                'renamed': final_name != cmd_upper
                            })
                            # Track ownership
                            owned_by_this_pack[final_name] = cmd_upper
                            command_ownership[final_name] = addon_name
                            # Update current_commands for subsequent conflict checks
                            current_commands[final_name] = {'rules': rules}
                            if final_name != cmd_upper:
                                renamed.append({
                                    'original': cmd_upper,
                                    'renamed_to': final_name,
                                    'reason': f'Name conflict with existing command'
                                })
                        else:
                            errors.append(f"Failed to add {cmd_upper}: {result.get('error')}")

                elif not is_selected:
                    # User wants this command removed
                    # Settings panel model: if user unchecks it, remove it
                    installed_name = None
                    other_pack_owner = None

                    if is_owned:
                        # Find the actual installed name (might have been renamed)
                        for inst_name, orig_name in owned_by_this_pack.items():
                            if orig_name == cmd_upper:
                                installed_name = inst_name
                                break
                    elif cmd_upper in current_commands:
                        # Command exists in domain - check ownership
                        owner = command_ownership.get(cmd_upper)
                        if owner and owner != addon_name:
                            # Owned by another pack - we'll remove and update that pack's registry
                            installed_name = cmd_upper
                            other_pack_owner = owner
                        elif not owner:
                            # Untracked - we can remove it
                            installed_name = cmd_upper

                    if installed_name:
                        # Check if command actually exists in domain before deleting
                        if installed_name in current_commands:
                            result = domains_manager.delete_command(installed_name)
                            if result.get('success'):
                                removed.append(installed_name)
                            else:
                                errors.append(f"Failed to remove {installed_name}: {result.get('error')}")
                        else:
                            # Command already gone from domain - just update registry
                            removed.append(installed_name)

                        # Clean up ownership tracking
                        if installed_name in owned_by_this_pack:
                            del owned_by_this_pack[installed_name]
                        if installed_name in current_commands:
                            del current_commands[installed_name]

                        # If this was owned by another pack, update their registry too
                        if other_pack_owner:
                            for other_addon in registry.get('addons', []):
                                if other_addon.get('name') == other_pack_owner:
                                    if other_addon.get('commands_owned') and installed_name in other_addon['commands_owned']:
                                        del other_addon['commands_owned'][installed_name]
                                    # Also check legacy format
                                    if other_addon.get('commands_added') and installed_name in other_addon['commands_added']:
                                        other_addon['commands_added'].remove(installed_name)
                                    break

                # If selected and already owned, no action needed (keep as is)

            # Update registry
            if addon_entry:
                # Update existing entry
                addon_entry['commands_owned'] = owned_by_this_pack
                addon_entry['updated_at'] = datetime.now().isoformat()
                registry['addons'][addon_entry_index] = addon_entry
            else:
                # Create new entry
                registry['addons'].append({
                    'name': addon_name,
                    'source_file': str(path),
                    'type': 'commands',
                    'domains': [],
                    'merged': ['COMMANDS'] if owned_by_this_pack else [],
                    'commands_owned': owned_by_this_pack,
                    'installed_at': datetime.now().isoformat()
                })

            self._save_installed_registry(registry)

            return {
                'success': len(errors) == 0,
                'addon_name': addon_name,
                'added': added,
                'removed': removed,
                'renamed': renamed,
                'active_commands': list(owned_by_this_pack.keys()),
                'errors': errors,
                # Debug info
                '_debug': {
                    'registry_entry_found': addon_entry is not None,
                    'had_commands_added': addon_entry.get('commands_added', []) if addon_entry else [],
                    'had_commands_owned': list(addon_entry.get('commands_owned', {}).keys()) if addon_entry else [],
                    'legacy_migrated': list(legacy_commands) if legacy_commands else [],
                    'selected_upper': list(selected_upper),
                    'pack_commands_count': len(pack_commands),
                    'current_commands_count': len(current_commands),
                    'all_command_ownership': command_ownership  # Shows who owns what
                }
            }

        except json.JSONDecodeError as e:
            return {'success': False, 'error': f'Invalid JSON: {e}'}
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _get_available_command_name(self, desired_name: str, current_commands: dict,
                                     ownership: dict, this_addon: str) -> str:
        """Find an available command name, appending numbers if needed.

        Args:
            desired_name: The command name we want.
            current_commands: Dict of existing commands.
            ownership: Dict of {command_name: addon_name} ownership.
            this_addon: Name of the addon requesting the name.

        Returns:
            Available name (desired_name, or desired_name1, desired_name2, etc.)
        """
        # If name doesn't exist, use it
        if desired_name not in current_commands or desired_name == '_order':
            return desired_name

        # If we own it, use it
        if ownership.get(desired_name) == this_addon:
            return desired_name

        # Name conflict - find available numbered version
        counter = 1
        while True:
            candidate = f"{desired_name}{counter}"
            if candidate not in current_commands and candidate not in ownership:
                return candidate
            counter += 1
            if counter > 99:  # Safety limit
                raise ValueError(f"Too many command name conflicts for {desired_name}")

    def _merge_rules_into_domain(self, domain_file: Path, domain_name: str,
                                  new_rules: list, addon_name: str) -> int:
        """Merge new rules into an existing domain file.

        Args:
            domain_file: Path to existing domain file.
            domain_name: Name of the domain (uppercase).
            new_rules: List of rule dicts to add.
            addon_name: Name of addon (for comments).

        Returns:
            Number of rules added.
        """
        upper = domain_name.upper()

        # Read existing content
        existing_content = domain_file.read_text()
        existing_lines = existing_content.splitlines()

        # Find highest existing rule index
        max_index = -1
        for line in existing_lines:
            if line.startswith(f'{upper}_RULE_') and '=' in line:
                try:
                    key = line.split('=', 1)[0]
                    idx = int(key.split('_')[-1])
                    max_index = max(max_index, idx)
                except ValueError:
                    continue

        # Add new rules starting after highest index
        new_lines = []
        new_lines.append("")
        new_lines.append(f"# Merged from addon: {addon_name}")

        added_count = 0
        for rule in new_rules:
            max_index += 1
            text = rule.get('text', '')
            new_lines.append(f"{upper}_RULE_{max_index}={text}")
            added_count += 1

        # Append to file
        new_content = existing_content.rstrip() + '\n' + '\n'.join(new_lines)
        domain_file.write_text(new_content)

        return added_count

    def _update_manifest_for_domain(self, domain_name: str, state: str = 'inactive',
                                     always_on: bool = False, recall: str = '',
                                     exclude: str = ''):
        """Add or update domain entries in manifest file."""
        manifest_path = self.carl_path / 'manifest'
        if not manifest_path.exists():
            return

        content = manifest_path.read_text()
        lines = content.splitlines()
        upper_name = domain_name.upper()

        # Keys to set
        keys_to_set = {
            f'{upper_name}_STATE': state,
            f'{upper_name}_ALWAYS_ON': 'true' if always_on else 'false',
        }
        if recall:
            keys_to_set[f'{upper_name}_RECALL'] = recall
        if exclude:
            keys_to_set[f'{upper_name}_EXCLUDE'] = exclude

        # Update existing keys or mark for addition
        keys_found = set()
        for i, line in enumerate(lines):
            stripped = line.strip()
            if '=' in stripped and not stripped.startswith('#'):
                key = stripped.split('=', 1)[0].strip()
                if key in keys_to_set:
                    lines[i] = f"{key}={keys_to_set[key]}"
                    keys_found.add(key)

        # Add missing keys
        for key, value in keys_to_set.items():
            if key not in keys_found:
                lines.append(f"{key}={value}")

        manifest_path.write_text('\n'.join(lines))

    def export_addon(self, domain_names: list, options: dict) -> dict:
        """Export selected domains to addon JSON format.

        Args:
            domain_names: List of domain names to export.
            options: Export metadata:
                - name: Addon name (required)
                - description: Addon description
                - author: Author name
                - tags: List of tags

        Returns:
            Dict with JSON content or error.
        """
        if not options.get('name'):
            return {'success': False, 'error': 'Addon name is required'}

        try:
            domains = []
            special_domains = {}

            # Read manifest for domain settings
            manifest_data = self._read_manifest()

            for domain_name in domain_names:
                domain_file = self.carl_path / domain_name.lower()
                if not domain_file.exists():
                    continue

                rules = self._parse_domain_file(domain_file, domain_name)

                # Check if special domain
                if domain_name.upper() in ['CONTEXT', 'COMMANDS']:
                    special_domains[domain_name.upper()] = {'rules': rules}
                else:
                    upper = domain_name.upper()
                    domain_entry = {
                        'name': upper,
                        'state': manifest_data.get(f'{upper}_STATE', 'inactive'),
                        'always_on': manifest_data.get(f'{upper}_ALWAYS_ON', 'false').lower() == 'true',
                        'recall': manifest_data.get(f'{upper}_RECALL', ''),
                        'exclude': manifest_data.get(f'{upper}_EXCLUDE', ''),
                        'rules': rules
                    }
                    domains.append(domain_entry)

            addon = {
                'version': ADDON_SCHEMA_VERSION,
                'type': 'carl-addon',
                'metadata': {
                    'name': options['name'],
                    'description': options.get('description', ''),
                    'author': options.get('author', ''),
                    'created': datetime.now().isoformat(),
                    'tags': options.get('tags', [])
                },
                'domains': domains,
                'special_domains': special_domains if special_domains else {},
                'manifest_settings': {},
                'optional_hooks': []
            }

            return {
                'success': True,
                'content': json.dumps(addon, indent=2),
                'filename': f"{options['name'].lower().replace(' ', '-')}.json"
            }

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

    def _read_manifest(self) -> dict:
        """Read manifest file into a dictionary."""
        manifest_path = self.carl_path / 'manifest'
        if not manifest_path.exists():
            return {}

        result = {}
        for line in manifest_path.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith('#') and '=' in line:
                key, value = line.split('=', 1)
                result[key.strip()] = value.strip()
        return result

    def _parse_domain_file(self, domain_path: Path, domain_name: str) -> list:
        """Parse a domain file and extract rules."""
        rules = []
        upper = domain_name.upper()
        content = domain_path.read_text()

        # CONTEXT uses bracket-based rules (FRESH_RULE_*, MODERATE_RULE_*, DEPLETED_RULE_*)
        if upper == 'CONTEXT':
            import re
            pattern = r'^(FRESH|MODERATE|DEPLETED)_RULE_(\d+)\s*=\s*(.*)$'
            for line in content.splitlines():
                match = re.match(pattern, line.strip(), re.IGNORECASE)
                if match:
                    bracket = match.group(1).upper()
                    idx = int(match.group(2))
                    text = match.group(3)
                    rules.append({'index': len(rules), 'text': f'[{bracket}] {text}'})
            return rules

        # Standard format: {DOMAIN}_RULE_N=text
        for line in content.splitlines():
            line = line.strip()
            if line.startswith(f'{upper}_RULE_') and '=' in line:
                key, value = line.split('=', 1)
                # Extract rule index from key like "DOMAIN_RULE_0"
                try:
                    idx = int(key.split('_')[-1])
                    rules.append({'index': idx, 'text': value})
                except ValueError:
                    continue

        return sorted(rules, key=lambda r: r['index'])

    def preview_uninstall(self, addon_name: str) -> dict:
        """Preview what would be uninstalled for an addon.

        Args:
            addon_name: Name of the addon.

        Returns:
            Dict with domains and commands that would be affected.
        """
        try:
            registry = self._load_installed_registry()
            addon_entry = None

            for addon in registry['addons']:
                if addon.get('name') == addon_name:
                    addon_entry = addon
                    break

            if not addon_entry:
                return {'success': False, 'error': f'Addon not found: {addon_name}'}

            # Get domains
            domains = addon_entry.get('domains', [])

            # Get commands with their details
            commands = []
            if addon_entry.get('commands_owned'):
                from .domains import DomainManager
                domains_manager = DomainManager(self.carl_path)
                commands_file = self.carl_path / 'commands'
                current_commands = {}
                if commands_file.exists():
                    current_commands = domains_manager._parse_commands(commands_file)

                for installed_name, original_name in addon_entry['commands_owned'].items():
                    cmd_data = current_commands.get(installed_name, {})
                    rules = cmd_data.get('rules', [])
                    commands.append({
                        'name': installed_name,
                        'original_name': original_name,
                        'rule_count': len(rules),
                        'rules': rules,
                        'exists': installed_name in current_commands
                    })

            return {
                'success': True,
                'addon_name': addon_name,
                'addon_type': addon_entry.get('type', 'standard'),
                'domains': domains,
                'commands': commands,
                'is_commands_addon': len(commands) > 0 and len(domains) == 0
            }

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

    def uninstall_addon(self, addon_name: str, command_action: str = 'remove_all',
                        commands_to_remove: list = None) -> dict:
        """Remove installed addon's domains and optionally commands.

        Args:
            addon_name: Name of the addon to uninstall.
            command_action: How to handle commands - 'keep_all', 'remove_all', or 'selected'.
            commands_to_remove: List of command names to remove (only used if action is 'selected').

        Returns:
            Dict with uninstallation results.
        """
        try:
            registry = self._load_installed_registry()
            addon_entry = None

            for addon in registry['addons']:
                if addon.get('name') == addon_name:
                    addon_entry = addon
                    break

            if not addon_entry:
                return {'success': False, 'error': f'Addon not found: {addon_name}'}

            removed_domains = []
            removed_commands = []
            kept_commands = []
            errors = []

            # Remove domain files
            for domain_name in addon_entry.get('domains', []):
                domain_file = self.carl_path / domain_name.lower()
                if domain_file.exists():
                    try:
                        domain_file.unlink()
                        removed_domains.append(domain_name)
                    except Exception as e:
                        errors.append(f"{domain_name}: {e}")
                else:
                    removed_domains.append(domain_name)  # Already gone

            # Handle commands based on action
            if addon_entry.get('commands_owned') and command_action != 'keep_all':
                from .domains import DomainManager
                domains_manager = DomainManager(self.carl_path)

                commands_to_process = set()
                if command_action == 'remove_all':
                    commands_to_process = set(addon_entry['commands_owned'].keys())
                elif command_action == 'selected' and commands_to_remove:
                    commands_to_process = set(commands_to_remove)

                print(f"[UNINSTALL] commands_to_process: {commands_to_process}")
                print(f"[UNINSTALL] commands_owned: {addon_entry['commands_owned']}")

                for installed_name, original_name in addon_entry['commands_owned'].items():
                    print(f"[UNINSTALL] Checking {installed_name}: in commands_to_process = {installed_name in commands_to_process}")
                    if installed_name in commands_to_process:
                        result = domains_manager.delete_command(installed_name)
                        print(f"[UNINSTALL] delete_command({installed_name}) result: {result}")
                        if result.get('success'):
                            removed_commands.append(installed_name)
                        else:
                            # If command doesn't exist, still count as removed
                            if 'not found' in result.get('error', '').lower():
                                removed_commands.append(installed_name)
                            else:
                                errors.append(f"Command {installed_name}: {result.get('error')}")
                    else:
                        kept_commands.append(installed_name)

            elif addon_entry.get('commands_owned') and command_action == 'keep_all':
                kept_commands = list(addon_entry['commands_owned'].keys())

            # Remove from registry
            registry['addons'] = [a for a in registry['addons'] if a.get('name') != addon_name]
            self._save_installed_registry(registry)

            return {
                'success': len(errors) == 0,
                'removed': removed_domains,
                'removed_commands': removed_commands,
                'kept_commands': kept_commands,
                'errors': errors
            }

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

    def get_exportable_domains(self) -> dict:
        """Get list of domains that can be exported.

        Returns:
            Dict with list of domain names and their info.
        """
        import re
        domains = []
        manifest_data = self._read_manifest()

        # Find all domain files
        excluded_names = {'manifest', 'sessions', 'manifest.env', 'sessions.env', 'installed.json'}
        excluded_extensions = {'.json', '.jsonc', '.md', '.txt', '.log'}

        for file_path in self.carl_path.iterdir():
            if not file_path.is_file():
                continue
            name = file_path.name
            if name.startswith('.') or name.lower() in excluded_names:
                continue
            if file_path.suffix.lower() in excluded_extensions:
                continue

            upper = name.upper()
            is_special = upper in ['CONTEXT', 'COMMANDS', 'GLOBAL']

            # Get state - special domains default to 'active' if file exists
            state_key = f'{upper}_STATE'
            state = manifest_data.get(state_key)
            if state is None:
                state = 'active' if is_special else 'inactive'

            domain_info = {
                'name': upper,
                'file': name,
                'state': state,
                'always_on': manifest_data.get(f'{upper}_ALWAYS_ON', 'false').lower() == 'true',
                'is_special': is_special
            }

            # Handle CONTEXT specially - get bracket counts
            if upper == 'CONTEXT':
                content = file_path.read_text()
                brackets = {'FRESH': 0, 'MODERATE': 0, 'DEPLETED': 0}
                pattern = r'^(FRESH|MODERATE|DEPLETED)_RULE_\d+\s*='
                for line in content.splitlines():
                    match = re.match(pattern, line.strip(), re.IGNORECASE)
                    if match:
                        bracket = match.group(1).upper()
                        brackets[bracket] += 1
                domain_info['brackets'] = brackets
                domain_info['rule_count'] = sum(brackets.values())

            # Handle COMMANDS specially - get command count and total rule count
            elif upper == 'COMMANDS':
                content = file_path.read_text()
                commands = {}
                pattern = r'^(\w+)_RULE_(\d+)\s*='
                for line in content.splitlines():
                    match = re.match(pattern, line.strip())
                    if match:
                        cmd = match.group(1).upper()
                        # Skip bracket names from context format
                        if cmd not in ['FRESH', 'MODERATE', 'DEPLETED']:
                            commands[cmd] = commands.get(cmd, 0) + 1
                domain_info['command_count'] = len(commands)
                domain_info['rule_count'] = sum(commands.values())

            # Standard domain
            else:
                rules = self._parse_domain_file(file_path, name)
                domain_info['rule_count'] = len(rules)

            domains.append(domain_info)

        # Sort: GLOBAL, CONTEXT, COMMANDS first (in that order), then user domains
        # User domain order comes from manifest DOMAIN_ORDER, fallback to alphabetical
        core_order = {'GLOBAL': 0, 'CONTEXT': 1, 'COMMANDS': 2}
        user_order_str = manifest_data.get('DOMAIN_ORDER', '')
        user_order_list = [d.strip().upper() for d in user_order_str.split(',') if d.strip()]
        user_order = {name: idx for idx, name in enumerate(user_order_list)}

        def sort_key(d):
            name = d['name']
            if name in core_order:
                return (0, core_order[name], name)  # Core domains first
            elif name in user_order:
                return (1, user_order[name], name)  # User-ordered domains
            else:
                return (2, 0, name)  # Remaining alphabetically

        return {
            'success': True,
            'domains': sorted(domains, key=sort_key)
        }

    def save_addon_to_available(self, content: str, filename: str) -> dict:
        """Save addon JSON content to the available directory.

        Args:
            content: JSON string of the addon
            filename: Filename to save as

        Returns:
            Dict with save result.
        """
        try:
            self._ensure_addons_dir()

            # Validate JSON
            json.loads(content)

            if not filename.endswith('.json'):
                filename += '.json'

            output_path = self.available_path / filename
            output_path.write_text(content)

            return {
                'success': True,
                'path': str(output_path)
            }
        except json.JSONDecodeError as e:
            return {'success': False, 'error': f'Invalid JSON: {e}'}
        except Exception as e:
            return {'success': False, 'error': str(e)}
