"""
DSS Project Manager

Handles project lifecycle operations: init, sync, build, list.

Projects inherit from DSS core (shadcn/ui) as the base layer.
The hierarchy is: DSS Core → Skins → Project customizations.
"""

import json
import os
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
import logging

from dss.project.models import (
    DSSProject,
    ProjectConfig,
    FigmaSource,
    FigmaFile,
    OutputConfig,
    ProjectStatus,
)
from dss.project.figma import FigmaProjectSync, FigmaStyleData, FigmaRateLimitError
from dss.project.core import (
    DSS_FIGMA_REFERENCE,
    DSS_CORE_THEMES,
    DSS_CORE_COMPONENTS,
)
from dss.project.sync import DSSCoreSync, get_dss_core_tokens, get_dss_core_themes

logger = logging.getLogger(__name__)

# Default location for DSS projects registry
DSS_PROJECTS_DIR = Path.home() / ".dss" / "projects"
DSS_REGISTRY_FILE = Path.home() / ".dss" / "registry.json"


class ProjectRegistry:
    """
    Global registry of DSS projects.

    Tracks all known projects across the system.
    """

    def __init__(self, registry_path: Optional[Path] = None):
        self.registry_path = registry_path or DSS_REGISTRY_FILE
        self._projects: Dict[str, Dict[str, Any]] = {}
        self._load()

    def _load(self):
        """Load registry from disk."""
        if self.registry_path.exists():
            try:
                with open(self.registry_path, "r") as f:
                    data = json.load(f)
                    self._projects = data.get("projects", {})
            except Exception as e:
                logger.warning(f"Failed to load registry: {e}")
                self._projects = {}
        else:
            self._projects = {}

    def _save(self):
        """Save registry to disk."""
        self.registry_path.parent.mkdir(parents=True, exist_ok=True)
        with open(self.registry_path, "w") as f:
            json.dump({
                "version": "1.0",
                "updated_at": datetime.now().isoformat(),
                "projects": self._projects,
            }, f, indent=2)

    def register(self, project: DSSProject):
        """Register a project."""
        self._projects[project.config.name] = {
            "name": project.config.name,
            "path": str(project.path),
            "status": project.status.value,
            "created_at": project.config.created_at.isoformat(),
            "updated_at": datetime.now().isoformat(),
        }
        self._save()

    def unregister(self, name: str):
        """Remove a project from registry."""
        if name in self._projects:
            del self._projects[name]
            self._save()

    def get(self, name: str) -> Optional[Dict[str, Any]]:
        """Get project info by name."""
        return self._projects.get(name)

    def get_by_path(self, path: Path) -> Optional[Dict[str, Any]]:
        """Get project info by path."""
        path_str = str(path.resolve())
        for proj in self._projects.values():
            if proj.get("path") == path_str:
                return proj
        return None

    def list_all(self) -> List[Dict[str, Any]]:
        """List all registered projects."""
        return list(self._projects.values())

    def update_status(self, name: str, status: ProjectStatus):
        """Update project status."""
        if name in self._projects:
            self._projects[name]["status"] = status.value
            self._projects[name]["updated_at"] = datetime.now().isoformat()
            self._save()


class ProjectManager:
    """
    Manages DSS project lifecycle.

    Operations:
    - init: Create a new project
    - add_figma: Link Figma sources
    - sync: Pull latest from sources
    - build: Generate output files
    - list: Show all projects
    """

    def __init__(self, registry: Optional[ProjectRegistry] = None):
        self.registry = registry or ProjectRegistry()

    # =========================================================================
    # Project Initialization
    # =========================================================================

    def init(
        self,
        path: Path,
        name: str,
        description: Optional[str] = None,
        skin: Optional[str] = None,
        base_theme: str = "light",
    ) -> DSSProject:
        """
        Initialize a new DSS project.

        Args:
            path: Directory for the project
            name: Project name
            description: Optional description
            skin: Base skin to extend (e.g., 'shadcn')
            base_theme: Default theme variant

        Returns:
            Initialized DSSProject
        """
        path = Path(path).resolve()

        # Check if already exists
        config_path = path / "ds.config.json"
        if config_path.exists():
            raise FileExistsError(f"Project already exists at {path}")

        # Create directory structure
        path.mkdir(parents=True, exist_ok=True)
        (path / "tokens").mkdir(exist_ok=True)
        (path / "tokens" / "figma").mkdir(exist_ok=True)
        (path / "tokens" / "custom").mkdir(exist_ok=True)
        (path / "tokens" / "compiled").mkdir(exist_ok=True)
        (path / "themes").mkdir(exist_ok=True)
        (path / "components").mkdir(exist_ok=True)

        # Create config
        config = ProjectConfig(
            name=name,
            description=description,
            skin=skin,
            base_theme=base_theme,
            output=OutputConfig(
                tokens_dir="./tokens/compiled",
                themes_dir="./themes",
                components_dir="./components",
            ),
        )

        # Create project
        project = DSSProject(
            config=config,
            path=path,
            status=ProjectStatus.CREATED,
        )

        # Save config file
        self._save_config(project)

        # Register project
        self.registry.register(project)

        logger.info(f"Initialized DSS project '{name}' at {path}")
        return project

    # =========================================================================
    # Figma Integration
    # =========================================================================

    def add_figma_team(
        self,
        project: DSSProject,
        team_id: str,
        figma_token: Optional[str] = None,
        auto_find_uikit: bool = True,
    ) -> DSSProject:
        """
        Link a Figma team folder to DSS project.

        The team folder is the main Figma resource. This method:
        1. Discovers all projects in the team
        2. Lists all files
        3. Auto-identifies the UIKit reference file

        Args:
            project: DSS project to update
            team_id: Figma team ID
            figma_token: Optional Figma token
            auto_find_uikit: Auto-search for UIKit file

        Returns:
            Updated project with team structure
        """
        sync = FigmaProjectSync(token=figma_token)

        # Discover full team structure
        team_structure = sync.discover_team_structure(team_id)

        # Create or update Figma source
        if project.config.figma is None:
            project.config.figma = FigmaSource(team_id=team_id)
        else:
            project.config.figma.team_id = team_id

        # Add all files from all projects
        for figma_project in team_structure.get("projects", []):
            project.config.figma.project_id = figma_project["id"]
            project.config.figma.project_name = figma_project["name"]

            for file_data in figma_project.get("files", []):
                project.config.figma.add_file(
                    key=file_data["key"],
                    name=file_data["name"],
                    thumbnail_url=file_data.get("thumbnail_url"),
                )

        # Set UIKit reference if found
        uikit_info = team_structure.get("uikit")
        if uikit_info:
            project.config.figma.uikit_file_key = uikit_info["key"]
            logger.info(f"Found UIKit file: '{uikit_info['name']}' in project '{uikit_info['project_name']}'")

        total_files = sum(len(p.get("files", [])) for p in team_structure.get("projects", []))
        project.config.updated_at = datetime.now()
        project.status = ProjectStatus.CONFIGURED

        self._save_config(project)
        self.registry.update_status(project.config.name, project.status)

        logger.info(f"Added Figma team {team_id} with {len(team_structure.get('projects', []))} projects, {total_files} files")
        return project

    def add_figma_project(
        self,
        project: DSSProject,
        figma_project_id: str,
        figma_token: Optional[str] = None,
        auto_find_uikit: bool = True,
    ) -> DSSProject:
        """
        Link a Figma project to DSS project.

        Args:
            project: DSS project to update
            figma_project_id: Figma project ID
            figma_token: Optional Figma token (uses env var if not provided)
            auto_find_uikit: Auto-search for UIKit file

        Returns:
            Updated project with Figma files
        """
        sync = FigmaProjectSync(token=figma_token)

        # Get project files from Figma
        project_data = sync.list_project_files(figma_project_id)

        # Create or update Figma source
        if project.config.figma is None:
            project.config.figma = FigmaSource(project_id=figma_project_id)
        else:
            project.config.figma.project_id = figma_project_id

        # Add all files
        uikit_key = None
        for file_data in project_data["files"]:
            project.config.figma.add_file(
                key=file_data["key"],
                name=file_data["name"],
                thumbnail_url=file_data.get("thumbnail_url"),
            )

            # Look for UIKit file
            if auto_find_uikit and uikit_key is None:
                file_name_lower = file_data["name"].lower()
                if any(pattern in file_name_lower for pattern in [
                    "uikit", "ui-kit", "ui kit",
                    "design system", "design-system",
                    "tokens", "foundations",
                ]):
                    uikit_key = file_data["key"]
                    logger.info(f"Found UIKit file: '{file_data['name']}'")

        if uikit_key:
            project.config.figma.uikit_file_key = uikit_key

        project.config.updated_at = datetime.now()
        project.status = ProjectStatus.CONFIGURED

        # Save and update registry
        self._save_config(project)
        self.registry.update_status(project.config.name, project.status)

        logger.info(f"Added Figma project {figma_project_id} with {len(project_data['files'])} files")
        return project

    def add_figma_file(
        self,
        project: DSSProject,
        file_key: str,
        file_name: str,
        figma_token: Optional[str] = None,
    ) -> DSSProject:
        """
        Add a single Figma file to DSS project.

        Args:
            project: DSS project to update
            file_key: Figma file key
            file_name: Human-readable name for the file
            figma_token: Optional Figma token

        Returns:
            Updated project
        """
        if project.config.figma is None:
            project.config.figma = FigmaSource()

        project.config.figma.add_file(key=file_key, name=file_name)
        project.config.updated_at = datetime.now()

        self._save_config(project)
        logger.info(f"Added Figma file '{file_name}' ({file_key})")
        return project

    # =========================================================================
    # Sync Operations
    # =========================================================================

    def sync(
        self,
        project: DSSProject,
        figma_token: Optional[str] = None,
        file_keys: Optional[List[str]] = None,
    ) -> DSSProject:
        """
        Sync project from all sources (sync version).

        Uses rate limit handling with exponential backoff for Figma API.

        Args:
            project: Project to sync
            figma_token: Optional Figma token
            file_keys: Optional specific file keys to sync

        Returns:
            Updated project with extracted tokens

        Raises:
            FigmaRateLimitError: If rate limit exceeded after all retries
        """
        if project.config.figma is None or not project.config.figma.files:
            logger.warning("No Figma sources configured")
            return project

        sync = FigmaProjectSync(token=figma_token)

        # Determine which files to sync
        if file_keys is None:
            file_keys = [f.key for f in project.config.figma.files]

        # Extract from each file
        all_tokens: Dict[str, Any] = {"sources": {}}

        for file_key in file_keys:
            try:
                style_data = sync.get_file_styles(file_key)
                tokens = sync.to_dss_tokens(style_data)
                all_tokens["sources"][file_key] = tokens

                # Save raw tokens
                figma_dir = project.path / "tokens" / "figma"
                figma_dir.mkdir(parents=True, exist_ok=True)

                file_info = project.config.figma.get_file(file_key)
                file_name = file_info.name if file_info else file_key
                safe_name = file_name.replace("/", "-").replace(" ", "_").lower()

                sync.save_tokens(style_data, figma_dir / safe_name, format="json")
                sync.save_tokens(style_data, figma_dir / safe_name, format="raw")

                # Update sync timestamp
                if file_info:
                    file_info.last_synced = datetime.now()

                logger.info(f"Synced {len(tokens.get('tokens', {}))} tokens from '{file_name}'")

            except Exception as e:
                logger.error(f"Failed to sync file {file_key}: {e}")
                project.errors.append(f"Sync failed for {file_key}: {str(e)}")

        project.extracted_tokens = all_tokens
        project.config.updated_at = datetime.now()
        project.status = ProjectStatus.SYNCED

        self._save_config(project)
        self.registry.update_status(project.config.name, project.status)

        return project

    async def sync_async(
        self,
        project: DSSProject,
        figma_token: Optional[str] = None,
        file_keys: Optional[List[str]] = None,
    ) -> DSSProject:
        """
        Sync project from all sources (async version).

        Fetches from multiple files in parallel.
        """
        if project.config.figma is None or not project.config.figma.files:
            logger.warning("No Figma sources configured")
            return project

        sync = FigmaProjectSync(token=figma_token)

        try:
            # Determine which files to sync
            if file_keys is None:
                file_keys = [f.key for f in project.config.figma.files]

            # Parallel sync
            styles_map = await sync.sync_project_files_async(
                project.config.figma.project_id or "",
                file_keys=file_keys
            )

            # Process results
            all_tokens: Dict[str, Any] = {"sources": {}}
            figma_dir = project.path / "tokens" / "figma"
            figma_dir.mkdir(parents=True, exist_ok=True)

            for file_key, style_data in styles_map.items():
                tokens = sync.to_dss_tokens(style_data)
                all_tokens["sources"][file_key] = tokens

                # Save tokens
                file_info = project.config.figma.get_file(file_key)
                file_name = file_info.name if file_info else file_key
                safe_name = file_name.replace("/", "-").replace(" ", "_").lower()

                sync.save_tokens(style_data, figma_dir / safe_name, format="json")

                if file_info:
                    file_info.last_synced = datetime.now()

                logger.info(f"Synced {len(tokens.get('tokens', {}))} tokens from '{file_name}'")

            project.extracted_tokens = all_tokens
            project.config.updated_at = datetime.now()
            project.status = ProjectStatus.SYNCED

            self._save_config(project)
            self.registry.update_status(project.config.name, project.status)

        finally:
            await sync.close()

        return project

    # =========================================================================
    # Build Operations
    # =========================================================================

    def build(self, project: DSSProject, include_core: bool = True) -> DSSProject:
        """
        Build output files from synced tokens.

        Generates CSS, SCSS, JSON outputs based on project config.
        Inheritance order: DSS Core → Skin → Project tokens.

        Args:
            project: Project to build
            include_core: Whether to include DSS core tokens as base layer (default True)

        Returns:
            Updated project
        """
        if project.extracted_tokens is None:
            raise ValueError("No tokens to build. Run sync first.")

        output_dir = project.path / project.config.output.tokens_dir
        output_dir.mkdir(parents=True, exist_ok=True)

        # Start with DSS core tokens as base layer
        merged_tokens: Dict[str, Any] = {}

        if include_core:
            core_tokens = get_dss_core_tokens()
            if core_tokens:
                # Flatten core tokens into merged tokens
                for category, tokens in core_tokens.get("categories", {}).items():
                    for token_path, token_data in tokens.items():
                        full_path = f"{category}.{token_path}"
                        merged_tokens[full_path] = {
                            "value": token_data.get("value"),
                            "type": category,
                            "source": "dss-core",
                            "metadata": token_data,
                        }
                logger.info(f"Loaded {len(merged_tokens)} DSS core tokens as base layer")
            else:
                logger.warning("DSS core tokens not available. Using DSS default themes.")
                # Use default themes from core.py
                for theme_name, theme_data in DSS_CORE_THEMES.items():
                    for color_name, color_value in theme_data.get("colors", {}).items():
                        merged_tokens[f"color.{theme_name}.{color_name}"] = {
                            "value": f"hsl({color_value})",
                            "type": "color",
                            "source": "dss-defaults",
                        }

        # Merge project tokens on top (project overrides core)
        for source_tokens in project.extracted_tokens.get("sources", {}).values():
            merged_tokens.update(source_tokens.get("tokens", {}))

        # Generate each format
        for fmt in project.config.output.formats:
            try:
                output_file = output_dir / f"tokens.{fmt}"

                if fmt == "json":
                    self._generate_json(merged_tokens, output_file)
                elif fmt == "css":
                    self._generate_css(merged_tokens, output_file)
                elif fmt == "scss":
                    self._generate_scss(merged_tokens, output_file)
                elif fmt in ("js", "ts"):
                    self._generate_js(merged_tokens, output_file, typescript=(fmt == "ts"))

                logger.info(f"Generated {output_file}")

            except Exception as e:
                logger.error(f"Failed to generate {fmt}: {e}")
                project.errors.append(f"Build failed for {fmt}: {str(e)}")

        project.config.updated_at = datetime.now()
        project.status = ProjectStatus.BUILT

        self._save_config(project)
        self.registry.update_status(project.config.name, project.status)

        return project

    def _generate_json(self, tokens: Dict[str, Any], output_path: Path):
        """Generate JSON output."""
        with open(output_path, "w") as f:
            json.dump(tokens, f, indent=2)

    def _generate_css(self, tokens: Dict[str, Any], output_path: Path):
        """Generate CSS custom properties."""
        lines = [":root {"]
        for token_path, token_data in tokens.items():
            css_var = "--" + token_path.replace(".", "-")
            value = token_data.get("value", "/* unresolved */")
            if isinstance(value, dict):
                value = "/* complex value */"
            lines.append(f"  {css_var}: {value};")
        lines.append("}")

        with open(output_path, "w") as f:
            f.write("\n".join(lines))

    def _generate_scss(self, tokens: Dict[str, Any], output_path: Path):
        """Generate SCSS variables."""
        lines = []
        for token_path, token_data in tokens.items():
            scss_var = "$" + token_path.replace(".", "-")
            value = token_data.get("value", "null")
            if isinstance(value, dict):
                value = "null"
            lines.append(f"{scss_var}: {value};")

        with open(output_path, "w") as f:
            f.write("\n".join(lines))

    def _generate_js(self, tokens: Dict[str, Any], output_path: Path, typescript: bool = False):
        """Generate JS/TS module."""
        # Build nested object
        token_obj: Dict[str, Any] = {}
        for token_path, token_data in tokens.items():
            parts = token_path.split(".")
            current = token_obj
            for part in parts[:-1]:
                if part not in current:
                    current[part] = {}
                current = current[part]
            current[parts[-1]] = token_data.get("value")

        # Generate code
        if typescript:
            content = f"export const tokens = {json.dumps(token_obj, indent=2)} as const;\n"
        else:
            content = f"export const tokens = {json.dumps(token_obj, indent=2)};\n"

        with open(output_path, "w") as f:
            f.write(content)

    # =========================================================================
    # Project Loading
    # =========================================================================

    def load(self, path: Path) -> DSSProject:
        """Load an existing project from path."""
        path = Path(path).resolve()
        config_path = path / "ds.config.json"

        if not config_path.exists():
            raise FileNotFoundError(f"No ds.config.json found at {path}")

        return DSSProject.from_config_file(config_path)

    def load_by_name(self, name: str) -> DSSProject:
        """Load a project by name from registry."""
        project_info = self.registry.get(name)
        if project_info is None:
            raise ValueError(f"Project '{name}' not found in registry")

        return self.load(Path(project_info["path"]))

    def list(self) -> List[Dict[str, Any]]:
        """List all registered projects."""
        return self.registry.list_all()

    # =========================================================================
    # Helpers
    # =========================================================================

    def _save_config(self, project: DSSProject):
        """Save project config to ds.config.json."""
        config_dict = project.to_config_dict()
        with open(project.config_path, "w") as f:
            json.dump(config_dict, f, indent=2)
