"""Legacy data migrator for CleverAgents.

This module handles migrating data from legacy JSON files to the SQLite database.
Used for projects that were created before the database migration.
"""

import json
from datetime import datetime
from pathlib import Path
from typing import Any

from cleveragents.domain.models.core import (
    Change,
    Context,
    ContextType,
    OperationType,
    Plan,
    PlanBuild,
    PlanStatus,
    Project,
    ProjectSettings,
)
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork


class LegacyDataMigrator:
    """Migrates legacy JSON data to SQLite database."""

    def __init__(self, unit_of_work: UnitOfWork) -> None:
        """Initialize the legacy data migrator.

        Args:
            unit_of_work: Unit of Work for database transactions
        """
        self.unit_of_work = unit_of_work

    def migrate_project_data(self, project_path: Path) -> bool:
        """Migrate legacy JSON data for a project to SQLite.

        Args:
            project_path: Path to the project directory

        Returns:
            True if migration was performed, False if no legacy data found
        """
        try:
            cleveragents_dir = project_path / ".cleveragents"

            if not cleveragents_dir.exists():
                return False

            # Check for legacy JSON files
            plans_json = cleveragents_dir / "plans.json"
            contexts_json = cleveragents_dir / "contexts.json"
            changes_json = cleveragents_dir / "changes.json"

            # If none of the JSON files exist, nothing to migrate
            has_legacy = any(
                [plans_json.exists(), contexts_json.exists(), changes_json.exists()]
            )
            if not has_legacy:
                return False

            # Start migration
            with self.unit_of_work.transaction() as ctx:
                # First, create or get the project
                project_name_file = cleveragents_dir / "project.name"
                if project_name_file.exists():
                    project_name = project_name_file.read_text().strip()
                else:
                    project_name = project_path.name

                # Check if project already exists
                existing_project = ctx.projects.get_by_name(project_name)
                if existing_project:
                    project = existing_project
                else:
                    # Create new project
                    project = Project(
                        id=None,
                        name=project_name,
                        path=project_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",
                        ),
                    )
                    project = ctx.projects.create(project)
                    ctx.flush()  # Get the project ID

                # Migrate plans
                plan_id_map: dict[str, int] = {}  # Map old plan names to new IDs

                if plans_json.exists() and project.id:
                    with open(plans_json) as f:
                        plans_data = json.load(f)

                    # Fetch all existing plans once before the loop to avoid
                    # N+1 database queries (one query per plan iteration).
                    all_plans = ctx.plans.get_all_for_project(project.id)
                    for plan_name, plan_info in plans_data.items():
                        # Check if plan already exists
                        existing_plan = next(
                            (p for p in all_plans if p.name == plan_name), None
                        )
                        if existing_plan:
                            plan_id_map[plan_name] = existing_plan.id  # type: ignore
                            continue

                        # Create new plan
                        # Build plan dict with only fields that have values
                        plan_data: dict[str, Any] = {
                            "id": None,
                            "project_id": project.id,
                            "name": plan_name,
                            "prompt": plan_info.get("prompt", ""),
                            "status": self._map_plan_status(
                                plan_info.get("status", "pending")
                            ),
                            "current": plan_info.get("current", False),
                            "build": self._map_plan_build(plan_info.get("build")),
                        }

                        # Add optional datetime fields only if they have values
                        if created_at := self._parse_datetime(
                            plan_info.get("created_at")
                        ):
                            plan_data["created_at"] = created_at
                        if updated_at := self._parse_datetime(
                            plan_info.get("updated_at")
                        ):
                            plan_data["updated_at"] = updated_at
                        if build_started := self._parse_datetime(
                            plan_info.get("build_started_at")
                        ):
                            plan_data["build_started_at"] = build_started
                        if build_completed := self._parse_datetime(
                            plan_info.get("build_completed_at")
                        ):
                            plan_data["build_completed_at"] = build_completed
                        if applied_at := self._parse_datetime(
                            plan_info.get("applied_at")
                        ):
                            plan_data["applied_at"] = applied_at

                        # Add optional non-datetime fields
                        if model_used := plan_info.get("model_used"):
                            plan_data["model_used"] = model_used
                        if token_count := plan_info.get("token_count"):
                            plan_data["token_count"] = token_count
                        # Skip 'result' field as it requires PlanResult object
                        if files_created := plan_info.get("files_created"):
                            plan_data["files_created"] = files_created
                        if files_modified := plan_info.get("files_modified"):
                            plan_data["files_modified"] = files_modified
                        if files_deleted := plan_info.get("files_deleted"):
                            plan_data["files_deleted"] = files_deleted

                        plan = Plan(**plan_data)
                        created_plan = ctx.plans.create(plan)
                        ctx.flush()

                        if created_plan.id:
                            plan_id_map[plan_name] = created_plan.id

                            # Set current plan if needed
                            if plan.current:
                                ctx.plans.set_current(project.id, created_plan.id)
                                project.current_plan_id = created_plan.id
                                ctx.projects.update(project)

                # Get current plan name for context/changes migration
                current_plan_name = None
                current_file = cleveragents_dir / "current"
                if current_file.exists():
                    current_plan_name = current_file.read_text().strip()

                current_plan_id = (
                    plan_id_map.get(current_plan_name) if current_plan_name else None
                )

                # Migrate contexts
                if contexts_json.exists() and current_plan_id:
                    with open(contexts_json) as f:
                        contexts_data = json.load(f)

                    for file_path, context_info in contexts_data.items():
                        added_at = self._parse_datetime(context_info.get("added_at"))
                        context = Context(
                            id=None,
                            plan_id=current_plan_id,
                            path=file_path,
                            content=context_info.get("content"),
                            file_hash=context_info.get("file_hash"),
                            size=context_info.get("size", 0),
                            type=self._map_context_type(
                                context_info.get("type", "file")
                            ),
                            added_at=added_at if added_at else datetime.now(),
                        )
                        ctx.contexts.add(context)

                # Migrate changes
                if changes_json.exists() and current_plan_id:
                    with open(changes_json) as f:
                        changes_data = json.load(f)

                    for change_info in changes_data:
                        created_at = self._parse_datetime(change_info.get("created_at"))
                        change = Change(
                            id=None,
                            plan_id=current_plan_id,
                            file_path=change_info.get("file_path", ""),
                            operation=self._map_operation(
                                change_info.get("operation", "create")
                            ),
                            original_content=change_info.get("original_content"),
                            new_content=change_info.get("new_content"),
                            new_path=change_info.get("new_path"),
                            applied=change_info.get("applied", False),
                            applied_at=self._parse_datetime(
                                change_info.get("applied_at")
                            ),
                            created_at=created_at if created_at else datetime.now(),
                        )
                        ctx.changes.add(change)

            # After successful migration, rename JSON files to .backup
            if plans_json.exists():
                plans_json.rename(plans_json.with_suffix(".json.backup"))
            if contexts_json.exists():
                contexts_json.rename(contexts_json.with_suffix(".json.backup"))
            if changes_json.exists():
                changes_json.rename(changes_json.with_suffix(".json.backup"))

            return True
        except (json.JSONDecodeError, OSError):
            # Handle JSON parsing errors, file system errors gracefully
            return False

    def _map_plan_status(self, status: str) -> PlanStatus:
        """Map legacy status string to PlanStatus enum."""
        status_map = {
            "pending": PlanStatus.PENDING,
            "building": PlanStatus.BUILDING,
            "built": PlanStatus.BUILT,
            "failed": PlanStatus.ERROR,
            "error": PlanStatus.ERROR,
            "applied": PlanStatus.APPLIED,
        }
        return status_map.get(status.lower(), PlanStatus.PENDING)

    def _map_plan_build(self, build_data: Any) -> PlanBuild | None:
        """Map legacy build data to PlanBuild.

        Legacy data doesn't have the required fields for PlanBuild,
        so we return None and use backward compatibility fields instead.
        """
        # Legacy data structure is incompatible with current PlanBuild model
        # Return None to use backward compatibility fields in Plan
        return None

    def _map_context_type(self, type_str: str) -> ContextType:
        """Map legacy context type string to ContextType enum."""
        type_map = {
            "file": ContextType.FILE,
            "directory": ContextType.DIRECTORY,
            "url": ContextType.URL,
            "document": ContextType.NOTE,  # Map legacy "document" to NOTE
            "note": ContextType.NOTE,
        }
        return type_map.get(type_str.lower(), ContextType.FILE)

    def _map_operation(self, op_str: str) -> OperationType:
        """Map legacy operation string to OperationType enum."""
        op_map = {
            "create": OperationType.CREATE,
            "modify": OperationType.MODIFY,
            "delete": OperationType.DELETE,
            "rename": OperationType.MOVE,  # Map legacy "rename" to MOVE
            "move": OperationType.MOVE,
        }
        return op_map.get(op_str.lower(), OperationType.CREATE)

    def _parse_datetime(self, dt_str: str | None) -> datetime | None:
        """Parse datetime string to datetime object."""
        if not dt_str:
            return None

        try:
            # Try ISO format first
            return datetime.fromisoformat(dt_str)
        except (ValueError, AttributeError):
            try:
                # Try common datetime format
                return datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
            except (ValueError, AttributeError):
                return None


def check_and_migrate_legacy_data(project_path: Path, unit_of_work: UnitOfWork) -> bool:
    """Check for legacy data and migrate if found.

    Args:
        project_path: Path to the project directory
        unit_of_work: Unit of Work for database transactions

    Returns:
        True if migration was performed, False otherwise
    """
    migrator = LegacyDataMigrator(unit_of_work)
    return migrator.migrate_project_data(project_path)
