import os
import threading
import logging
from typing import Optional, Dict, List, Any
from datetime import datetime
import uuid
from contextlib import contextmanager

from src.models.schemas import ProjectData
from src.config.settings import DB_PATH
from src.services.storage_service import storage_manager
from src.config.database import init_db
from src.repositories.project_repository import ProjectRepository

logger = logging.getLogger(__name__)


class ProjectManager:
    def __init__(self):
        # Ensure local data directory exists (handled by engine/config usually, but good to be safe)
        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)

        # Lock management for application-level concurrency safety
        self._locks: Dict[str, threading.Lock] = {}
        self._global_lock = threading.Lock()

        # Initialize DB
        init_db()
        
        # Initialize Repository
        self.repo = ProjectRepository()

    def _get_project_storage_path(self, project_id: str) -> str:
        return f"projects/{project_id}"

    @contextmanager
    def _lock_project(self, project_id: str):
        """
        Context manager to acquire a lock for a specific project.
        Ensures that concurrent updates to the same project are serialized.
        """
        with self._global_lock:
            if project_id not in self._locks:
                self._locks[project_id] = threading.Lock()
            lock = self._locks[project_id]

        with lock:
            yield

    def _generate_episodes_from_chapters(self, chapters: List[Dict]) -> List:
        """Helper to generate Episode objects from chapter dictionaries."""
        episodes = []
        if not chapters:
            return episodes
            
        from src.models.schemas import Episode
        for i, chapter in enumerate(chapters):
            try:
                # Try to get order from chapter, or use index + 1
                order = chapter.get('chapter_number') or (i + 1)
                # Try to ensure order is an int
                if isinstance(order, str) and order.isdigit():
                    order = int(order)
                elif not isinstance(order, int):
                        order = i + 1
                
                episode = Episode(
                    id=chapter.get('id') or str(uuid.uuid4()),
                    title=chapter.get('title', f"Chapter {order}"),
                    order=order,
                    desc=chapter.get('summary') or chapter.get('description'),
                    content=chapter.get('content'),
                    status='draft'
                )
                episodes.append(episode)
            except Exception as e:
                logger.warning(f"Failed to create episode from chapter {i}: {e}")
        return episodes

    def create_project(
        self,
        name: str,
        description: Optional[str] = None,
        type: str = "video",
        chapters: Optional[List[Dict]] = None,
        assets: Optional[List[Dict]] = None,
        status: str = "active",
        user_id: Optional[str] = None
    ) -> ProjectData:
        project_id = str(uuid.uuid4())
        now = datetime.now()

        # Parse assets if provided
        parsed_assets = []
        if assets:
            from src.models.schemas import CharacterAsset, SceneAsset, PropAsset, OtherAsset
            from pydantic import TypeAdapter, ValidationError
            
            for asset_data in assets:
                try:
                    # Validate and convert to Asset Union
                    # Ensure ID exists
                    if 'id' not in asset_data:
                        asset_data['id'] = str(uuid.uuid4())
                    
                    # Basic type check
                    if 'type' not in asset_data:
                        asset_data['type'] = 'other'
                        
                    if asset_data['type'] == 'character':
                        parsed_assets.append(TypeAdapter(CharacterAsset).validate_python(asset_data))
                    elif asset_data['type'] == 'scene':
                        parsed_assets.append(TypeAdapter(SceneAsset).validate_python(asset_data))
                    elif asset_data['type'] == 'prop':
                        parsed_assets.append(TypeAdapter(PropAsset).validate_python(asset_data))
                    else:
                        parsed_assets.append(TypeAdapter(OtherAsset).validate_python(asset_data))
                except Exception as e:
                    logger.warning(f"Skipping invalid asset during project creation: {e}")

        # Generate episodes from chapters if available
        episodes = self._generate_episodes_from_chapters(chapters) if chapters else []

        project = ProjectData(
            id=project_id,
            name=name,
            description=description,
            type=type,
            created_at=now,
            updated_at=now,
            status=status,
            assets=parsed_assets,
            episodes=episodes,
            storyboards=[],
            chapters=chapters or [],
            user_id=user_id
        )

        # Create project directory for files
        project_path = self._get_project_storage_path(project_id)
        storage_manager.ensure_dir(project_path)
        storage_manager.ensure_dir(f"{project_path}/assets")
        storage_manager.ensure_dir(f"{project_path}/storyboards")

        self.save_project(project)
        return project

    def save_project(self, project: ProjectData):
        """
        Saves the project data using the repository.
        """
        self.repo.save(project)

    def get_project(self, project_id: str, include_assets: bool = True, include_referenced_assets: bool = False, user_id: Optional[str] = None) -> Optional[ProjectData]:
        """Get project details"""
        return self.repo.get(project_id, include_assets=include_assets, include_referenced_assets=include_referenced_assets, user_id=user_id)

    def get_project_by_user(self, project_id: str, user_id: str, include_assets: bool = True, include_referenced_assets: bool = False) -> Optional[ProjectData]:
        """Get project details filtered by user"""
        return self.repo.get(project_id, include_assets=include_assets, include_referenced_assets=include_referenced_assets, user_id=user_id)

    def list_assets(self, project_id: str, asset_type: Optional[str] = None, search_query: Optional[str] = None, limit: int = 50, offset: int = 0) -> Optional[Dict[str, Any]]:
        """List assets for a project with pagination"""
        return self.repo.list_assets(project_id, asset_type, search_query, limit, offset)

    def list_projects(self, limit: int = 50, offset: int = 0, user_id: Optional[str] = None) -> List[ProjectData]:
        """
        List projects from the repository with pagination.
        If user_id is provided, only returns projects belonging to that user.
        """
        if user_id:
            return self.repo.list_by_user(user_id=user_id, limit=limit, offset=offset)
        return self.repo.list(limit=limit, offset=offset)

    def count_projects(self, user_id: Optional[str] = None) -> int:
        """
        Get total number of active projects.
        If user_id is provided, only counts projects belonging to that user.
        """
        if user_id:
            return self.repo.count_by_user(user_id)
        return self.repo.count()

    def cleanup_stuck_projects(self):
        """
        Resets projects stuck in 'initializing' or 'processing' state to 'failed' on startup.
        """
        try:
            count = self.repo.reset_stuck_status()
            if count > 0:
                logger.info(f"Cleanup: Reset {count} stuck projects to 'failed' status.")
        except Exception as e:
            logger.error(f"Failed to cleanup stuck projects: {e}")

    def update_project(self, project_id: str, updates: Dict) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            # Auto-generate episodes if chapters are updated but episodes are not provided
            # This handles the case where project is created first, then chapters are added (e.g. via Wizard)
            if 'chapters' in updates and updates['chapters'] and 'episodes' not in updates:
                # Only generate if current project has no episodes to avoid overwriting existing work
                if not project.episodes:
                    new_episodes = self._generate_episodes_from_chapters(updates['chapters'])
                    if new_episodes:
                        updates['episodes'] = new_episodes

            # Apply updates
            updated_data = project.model_dump()
            updated_data.update(updates)
            updated_data['updated_at'] = datetime.now()
            
            new_project = ProjectData(**updated_data)
            self.save_project(new_project)
            return new_project

    def delete_project(self, project_id: str):
        with self._lock_project(project_id):
            # Clean up storage
            project_path = self._get_project_storage_path(project_id)
            if os.path.exists(project_path):
                import shutil
                try:
                    shutil.rmtree(project_path)
                    logger.info(f"Deleted project files at {project_path}")
                except Exception as e:
                    logger.warning(f"Failed to delete project files at {project_path}: {e}")
            
            # Use hard_delete to physically remove from DB
            self.repo.hard_delete(project_id)

    # --- Episode Management ---

    def add_episode(self, project_id: str, episode) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            project.episodes.append(episode)
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    def update_episode(self, project_id: str, episode_id: str, updated_episode) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            for i, ep in enumerate(project.episodes):
                if ep.id == episode_id:
                    project.episodes[i] = updated_episode
                    break
            
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    def delete_episode(self, project_id: str, episode_id: str) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            project.episodes = [ep for ep in project.episodes if ep.id != episode_id]
            # Also cascade delete storyboards for this episode
            project.storyboards = [sb for sb in project.storyboards if sb.episode_id != episode_id]
            
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    # --- Asset Management ---

    def add_asset(self, project_id: str, asset) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            # Check for duplicates (by ID or by Name+Type)
            exists = any(
                a.id == asset.id or (a.type == asset.type and a.name == asset.name)
                for a in project.assets
            )
            
            if not exists:
                # Atomically add asset via repo
                self.repo.add_asset(project_id, asset)
                # Re-fetch project to get updated state
                project = self.get_project(project_id)
            
            return project

    def batch_add_assets(self, project_id: str, assets: List) -> Optional[ProjectData]:
        """
        Add multiple assets at once, checking for duplicates.
        """
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            assets_to_add = []
            for asset in assets:
                exists = any(
                    a.id == asset.id or (a.type == asset.type and a.name == asset.name)
                    for a in project.assets
                )
                if not exists:
                    assets_to_add.append(asset)
            
            if assets_to_add:
                # Use optimized batch add in repo
                return self.repo.batch_add_assets(project_id, assets_to_add)
            
            return project

    def update_asset(self, project_id: str, asset_id: str, updated_asset) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            # Verify asset exists
            exists = any(a.id == asset_id for a in project.assets)
            if not exists:
                 # Should we raise error or return None? Existing logic returned None implicitly via loop fallthrough?
                 # Actually old logic just did nothing if not found.
                 pass
            else:
                 self.repo.update_asset(project_id, updated_asset)
                 project = self.get_project(project_id)

            return project

    def delete_asset(self, project_id: str, asset_id: str) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            self.repo.delete_asset(project_id, asset_id)
            return self.get_project(project_id)

    # --- Storyboard Management ---

    def add_storyboard(self, project_id: str, storyboard) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            project.storyboards.append(storyboard)
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    def update_storyboard(self, project_id: str, storyboard_id: str, updated_storyboard) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            for i, sb in enumerate(project.storyboards):
                if sb.id == storyboard_id:
                    project.storyboards[i] = updated_storyboard
                    break
            
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    def delete_storyboard(self, project_id: str, storyboard_id: str) -> Optional[ProjectData]:
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return None
            
            project.storyboards = [sb for sb in project.storyboards if sb.id != storyboard_id]
            project.updated_at = datetime.now()
            self.save_project(project)
            return project

    def delete_storyboards_by_episode(self, project_id: str, episode_id: str):
        with self._lock_project(project_id):
            project = self.get_project(project_id)
            if not project:
                return
            
            project.storyboards = [sb for sb in project.storyboards if sb.episode_id != episode_id]
            project.updated_at = datetime.now()
            self.save_project(project)

project_manager = ProjectManager()
