"""Project service for managing CleverAgents projects.

This service handles project initialization, configuration, and management.
Uses repository pattern and Unit of Work for persistence (ADR-007).
"""

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import structlog

from cleveragents.application.services.context_service import DEFAULT_IGNORE_PATTERNS
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import (
    FileSystemError,
    NotFoundError,
    ValidationError,
)
from cleveragents.domain.models.core import Plan, PlanStatus, Project, ProjectSettings
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType

if TYPE_CHECKING:
    from cleveragents.infrastructure.events.protocol import EventBus

_logger = structlog.get_logger(__name__)


class ProjectService:
    """Service for managing CleverAgents projects.

    This service provides methods for initializing, configuring, and
    managing projects throughout their lifecycle.
    """

    def __init__(
        self,
        settings: Settings,
        unit_of_work: UnitOfWork,
        event_bus: EventBus | None = None,
    ):
        """Initialize the project service.

        Args:
            settings: Application settings
            unit_of_work: Unit of Work for database transactions
            event_bus: Optional EventBus for domain event emission.
        """
        self.settings = settings
        self.unit_of_work = unit_of_work
        self._event_bus = event_bus
        # Optional search root to limit filesystem discovery (used in tests)
        self.search_root: Path | None = None

    def initialize_project(
        self,
        name: str,
        path: Path,
        force: bool = False,
        create_ignore_file: bool = False,
        apply_default_filters: bool = False,
    ) -> Project:
        """Initialize a new CleverAgents project.

        Args:
            name: Project name
            path: Project path
            force: Force reinitialization if project exists
            create_ignore_file: Whether to write a default .agentsignore
                in the project root

        Returns:
            Project: The initialized project

        Raises:
            ValidationError: If project already exists and force is False
            FileSystemError: If unable to create project directories
        """
        project_dir = path / ".cleveragents"

        # Check if already initialized
        if project_dir.exists() and not force:
            raise ValidationError(
                message=f"Project already initialized at {path}",
                details={"path": str(path), "use_force": "Add --force to reinitialize"},
            )

        # Create project structure
        try:
            project_dir.mkdir(parents=True, exist_ok=True)
            (project_dir / "db.sqlite").touch()
            (project_dir / "config.yaml").touch()
            (project_dir / "current").write_text("main")
            # Store the project name
            (project_dir / "project.name").write_text(name)

            if create_ignore_file:
                ignore_path = path / ".agentsignore"
                if not ignore_path.exists():
                    ignore_content = "\n".join(DEFAULT_IGNORE_PATTERNS) + "\n"
                    ignore_path.write_text(ignore_content, encoding="utf-8")
        except Exception as e:
            raise FileSystemError(
                message=f"Failed to create project structure: {e}",
                path=path,
            ) from e

        # Create project in database
        project = Project(
            id=None,
            name=name,
            path=path,
            created_at=datetime.now(),
            updated_at=datetime.now(),
            current_plan_id=None,
            settings=ProjectSettings(
                auto_build=False,
                auto_apply=False,
                confirm_apply=True,
                max_context_size=50 * 1024 * 1024,  # 50MB
                default_model="mock-gpt",
                include_paths=[],
                exclude_paths=list(DEFAULT_IGNORE_PATTERNS)
                if apply_default_filters
                else [],
            ),
        )

        # Initialize database and save project
        self.unit_of_work.init_database()

        # Check for and migrate legacy JSON data if it exists
        from cleveragents.infrastructure.database.legacy_migrator import (
            check_and_migrate_legacy_data,
        )

        migrated = check_and_migrate_legacy_data(path, self.unit_of_work)

        with self.unit_of_work.transaction() as ctx:
            # Check if project with this name already exists FIRST
            existing = ctx.projects.get_by_name(name)
            if existing and not force:
                # If we just migrated, return the existing project
                if migrated:
                    return existing
                raise ValidationError(
                    message=f"Project with name '{name}' already exists",
                    details={"name": name},
                )

            # If project was created during migration, just return it
            if migrated and existing:
                return existing

            # If force=True and project exists, delete it first
            if existing and force and existing.id:
                ctx.projects.delete(existing.id)
                # Flush to ensure delete is committed before creating new project
                ctx.flush()

            # Now create the project
            created_project = ctx.projects.create(project)

            # Create default "main" plan for the project
            if created_project.id:
                main_plan = Plan(
                    id=None,
                    project_id=created_project.id,
                    name="main",
                    prompt="Main development plan",
                    status=PlanStatus.PENDING,
                    current=True,
                    created_at=datetime.now(),
                    updated_at=datetime.now(),
                    build=None,
                    build_started_at=None,
                    build_completed_at=None,
                    model_used=None,
                    token_count=None,
                    result=None,
                    applied_at=None,
                    files_created=None,
                    files_modified=None,
                    files_deleted=None,
                )
                created_plan = ctx.plans.create(main_plan)
                if created_plan.id:
                    ctx.plans.set_current(created_project.id, created_plan.id)
                    created_project.current_plan_id = created_plan.id
                    ctx.projects.update(created_project)

        return created_project

    def get_current_project(self) -> Project | None:
        """Get the current project from the working directory.

        Returns:
            Project or None if no project found
        """
        path = Path.cwd().resolve()
        env_root = os.getenv("CLEVERAGENTS_PROJECT_SEARCH_ROOT")
        search_root = (
            Path(env_root).resolve() if env_root else getattr(self, "search_root", None)
        )
        limit = search_root.resolve() if isinstance(search_root, Path) else None

        # Look for .cleveragents directory
        while path != path.parent:
            if limit and not path.is_relative_to(limit):
                break

            if (path / ".cleveragents").exists():
                # Try to read the project name from file
                name_file = path / ".cleveragents" / "project.name"
                if name_file.exists():
                    project_name = name_file.read_text().strip()

                    # Look up in database
                    with self.unit_of_work.transaction() as ctx:
                        project = ctx.projects.get_by_name(project_name)
                        if project:
                            return project

                    # If not in database, return a temporary project object
                    # This handles projects that exist but haven't been
                    # migrated to DB yet
                    try:
                        return Project(
                            id=None,
                            name=project_name,
                            path=path,
                            created_at=datetime.now(),
                            updated_at=datetime.now(),
                            current_plan_id=None,
                            settings=ProjectSettings(
                                auto_build=False,
                                auto_apply=False,
                                confirm_apply=True,
                                max_context_size=50 * 1024 * 1024,
                                default_model="mock-gpt",
                            ),
                        )
                    except Exception:
                        return None
                else:
                    # Legacy project without name file
                    project_name = path.name

                    try:
                        return Project(
                            id=None,
                            name=project_name,
                            path=path,
                            created_at=datetime.now(),
                            updated_at=datetime.now(),
                            current_plan_id=None,
                            settings=ProjectSettings(
                                auto_build=False,
                                auto_apply=False,
                                confirm_apply=True,
                                max_context_size=50 * 1024 * 1024,
                                default_model="mock-gpt",
                            ),
                        )
                    except Exception:
                        return None
            path = path.parent

        return None

    def get_project_by_name(self, name: str) -> Project:
        """Get a project by name.

        Args:
            name: Project name

        Returns:
            Project

        Raises:
            NotFoundError: If project not found
        """
        with self.unit_of_work.transaction() as ctx:
            project = ctx.projects.get_by_name(name)
            if not project:
                raise NotFoundError(
                    resource_type="project",
                    resource_id=name,
                )
            return project

    def update_project(self, project: Project) -> Project:
        """Update a project.

        Args:
            project: Project to update

        Returns:
            Updated project
        """
        with self.unit_of_work.transaction() as ctx:
            return ctx.projects.update(project)

    def get_project_stats(self, project: Project) -> dict[str, Any]:
        """Get statistics for a project.

        Args:
            project: The project to get stats for

        Returns:
            Dict with project statistics
        """
        with self.unit_of_work.transaction() as ctx:
            # Get actual stats from database
            plans = ctx.plans.get_all_for_project(project.id) if project.id else []
            current_plan = (
                ctx.plans.get_current_for_project(project.id) if project.id else None
            )

            context_count = 0
            changes_count = 0

            if current_plan and current_plan.id:
                contexts = ctx.contexts.get_for_plan(current_plan.id)
                context_count = len(contexts)

                changes = ctx.changes.get_for_plan(current_plan.id)
                changes_count = len(changes)

            return {
                "plans": len(plans),
                "context_files": context_count,
                "changes": changes_count,
                "current_plan": current_plan.name if current_plan else "None",
            }

    def create_project(
        self,
        name: str,
        path: Path,
        force: bool = False,
        create_ignore_file: bool = False,
        apply_default_filters: bool = False,
    ) -> Project:
        """Create a new CleverAgents project (alias for initialize_project).

        This method is an alias for initialize_project to maintain API compatibility.

        Args:
            name: Project name
            path: Project path
            force: Force reinitialization if project exists

        Returns:
            Project: The created project

        Raises:
            ValidationError: If project already exists and force is False
            FileSystemError: If unable to create project directories
        """
        return self.initialize_project(
            name,
            path,
            force=force,
            create_ignore_file=create_ignore_file,
            apply_default_filters=apply_default_filters,
        )

    def get_project_by_path(self, path: Path) -> Project | None:
        """Get a project by its filesystem path.

        Args:
            path: Project path

        Returns:
            Project or None if not found
        """
        with self.unit_of_work.transaction() as ctx:
            all_projects = ctx.projects.get_all()
            for project in all_projects:
                if project.path == path:
                    return project
            return None

    def list_projects(self, order_by: str = "created_at") -> list[Project]:
        """List all projects with optional ordering.

        Args:
            order_by: Field to order by (e.g., "created_at", "name")

        Returns:
            List of projects
        """
        with self.unit_of_work.transaction() as ctx:
            projects = ctx.projects.get_all()

            # Sort projects based on order_by parameter
            if order_by == "name":
                return sorted(projects, key=lambda p: p.name)
            elif order_by == "created_at":
                return sorted(projects, key=lambda p: p.created_at)
            else:
                # Default to created_at ordering
                return sorted(projects, key=lambda p: p.created_at)

    def update_file_filters(
        self,
        project: Project,
        *,
        include_add: list[str] | None = None,
        exclude_add: list[str] | None = None,
        include_remove: list[str] | None = None,
        exclude_remove: list[str] | None = None,
        clear_include: bool = False,
        clear_exclude: bool = False,
    ) -> Project:
        """Update project include/exclude globs and persist."""

        def _dedup(seq: list[str]) -> list[str]:
            seen: set[str] = set()
            ordered: list[str] = []
            for item in seq:
                if item not in seen:
                    seen.add(item)
                    ordered.append(item)
            return ordered

        with self.unit_of_work.transaction() as ctx:
            refreshed = ctx.projects.get_by_name(project.name)
            if not refreshed:
                raise NotFoundError(
                    message="Project not found", details={"name": project.name}
                )

            settings = refreshed.settings
            includes = [] if clear_include else list(settings.include_paths)
            excludes = [] if clear_exclude else list(settings.exclude_paths)

            if include_add:
                includes.extend(include_add)
            if exclude_add:
                excludes.extend(exclude_add)
            if include_remove:
                includes = [p for p in includes if p not in include_remove]
            if exclude_remove:
                excludes = [p for p in excludes if p not in exclude_remove]

            settings.include_paths = _dedup(includes)
            settings.exclude_paths = _dedup(excludes)
            refreshed.settings = settings
            ctx.projects.update(refreshed)
            return refreshed

    def get_project_filters(self, project: Project) -> tuple[list[str], list[str]]:
        """Return include and exclude globs for a project."""

        with self.unit_of_work.transaction() as ctx:
            refreshed = ctx.projects.get_by_name(project.name)
            if not refreshed:
                raise NotFoundError(
                    message="Project not found", details={"name": project.name}
                )
            return (
                list(refreshed.settings.include_paths),
                list(refreshed.settings.exclude_paths),
            )

    def delete_project(self, project: Project) -> None:
        """Delete a project from the database.

        Args:
            project: The project to delete
        """
        with self.unit_of_work.transaction() as ctx:
            if project.id:
                ctx.projects.delete(project.id)
        if project.id and self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.ENTITY_DELETED,
                        details={
                            "entity_type": "project",
                            "entity_name": project.name,
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="ENTITY_DELETED")
