# app/init_db.py
"""
Async database initialization script.

Creates the first superuser if configured and doesn't already exist.
Seeds default agent types (production data) and demo data (when DEMO_MODE is enabled).
"""

import asyncio
import json
import logging
import random
from datetime import UTC, date, datetime, timedelta
from pathlib import Path

from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.core.database import SessionLocal, engine
from app.crud.syndarix.agent_type import agent_type as agent_type_crud
from app.crud.user import user as user_crud
from app.models.organization import Organization
from app.models.syndarix import AgentInstance, AgentType, Issue, Project, Sprint
from app.models.syndarix.enums import (
    AgentStatus,
    AutonomyLevel,
    ClientMode,
    IssuePriority,
    IssueStatus,
    IssueType,
    ProjectComplexity,
    ProjectStatus,
    SprintStatus,
)
from app.models.user import User
from app.models.user_organization import UserOrganization
from app.schemas.syndarix import AgentTypeCreate
from app.schemas.users import UserCreate

logger = logging.getLogger(__name__)

# Data file paths
DATA_DIR = Path(__file__).parent.parent / "data"
DEFAULT_AGENT_TYPES_PATH = DATA_DIR / "default_agent_types.json"
DEMO_DATA_PATH = DATA_DIR / "demo_data.json"


async def init_db() -> User | None:
    """
    Initialize database with first superuser if settings are configured and user doesn't exist.

    Returns:
        The created or existing superuser, or None if creation fails
    """
    # Use default values if not set in environment variables
    superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com"

    default_password = "AdminPassword123!"
    if settings.DEMO_MODE:
        default_password = "AdminPass1234!"

    superuser_password = settings.FIRST_SUPERUSER_PASSWORD or default_password

    if not settings.FIRST_SUPERUSER_EMAIL or not settings.FIRST_SUPERUSER_PASSWORD:
        logger.warning(
            "First superuser credentials not configured in settings. "
            f"Using defaults: {superuser_email}"
        )

    async with SessionLocal() as session:
        try:
            # Check if superuser already exists
            existing_user = await user_crud.get_by_email(session, email=superuser_email)

            if existing_user:
                logger.info(f"Superuser already exists: {existing_user.email}")
            else:
                # Create superuser if doesn't exist
                user_in = UserCreate(
                    email=superuser_email,
                    password=superuser_password,
                    first_name="Admin",
                    last_name="User",
                    is_superuser=True,
                )

                existing_user = await user_crud.create(session, obj_in=user_in)
                await session.commit()
                await session.refresh(existing_user)
                logger.info(f"Created first superuser: {existing_user.email}")

            # ALWAYS load default agent types (production data)
            await load_default_agent_types(session)

            # Only load demo data if in demo mode
            if settings.DEMO_MODE:
                await load_demo_data(session)

            return existing_user

        except Exception as e:
            await session.rollback()
            logger.error(f"Error initializing database: {e}")
            raise


def _load_json_file(path: Path):
    with open(path) as f:
        return json.load(f)


async def load_default_agent_types(session: AsyncSession) -> None:
    """
    Load default agent types from JSON file.

    These are production defaults - created only if they don't exist, never overwritten.
    This allows users to customize agent types without worrying about server restarts.
    """
    if not DEFAULT_AGENT_TYPES_PATH.exists():
        logger.warning(
            f"Default agent types file not found: {DEFAULT_AGENT_TYPES_PATH}"
        )
        return

    try:
        data = await asyncio.to_thread(_load_json_file, DEFAULT_AGENT_TYPES_PATH)

        for agent_type_data in data:
            slug = agent_type_data["slug"]

            # Check if agent type already exists
            existing = await agent_type_crud.get_by_slug(session, slug=slug)

            if existing:
                logger.debug(f"Agent type already exists: {agent_type_data['name']}")
                continue

            # Create the agent type
            agent_type_in = AgentTypeCreate(
                name=agent_type_data["name"],
                slug=slug,
                description=agent_type_data.get("description"),
                expertise=agent_type_data.get("expertise", []),
                personality_prompt=agent_type_data["personality_prompt"],
                primary_model=agent_type_data["primary_model"],
                fallback_models=agent_type_data.get("fallback_models", []),
                model_params=agent_type_data.get("model_params", {}),
                mcp_servers=agent_type_data.get("mcp_servers", []),
                tool_permissions=agent_type_data.get("tool_permissions", {}),
                is_active=agent_type_data.get("is_active", True),
                # Category and display fields
                category=agent_type_data.get("category"),
                icon=agent_type_data.get("icon", "bot"),
                color=agent_type_data.get("color", "#3B82F6"),
                sort_order=agent_type_data.get("sort_order", 0),
                typical_tasks=agent_type_data.get("typical_tasks", []),
                collaboration_hints=agent_type_data.get("collaboration_hints", []),
            )

            await agent_type_crud.create(session, obj_in=agent_type_in)
            logger.info(f"Created default agent type: {agent_type_data['name']}")

        logger.info("Default agent types loaded successfully")

    except Exception as e:
        logger.error(f"Error loading default agent types: {e}")
        raise


async def load_demo_data(session: AsyncSession) -> None:
    """
    Load demo data from JSON file.

    Only runs when DEMO_MODE is enabled. Creates demo organizations, users,
    projects, sprints, agent instances, and issues.
    """
    if not DEMO_DATA_PATH.exists():
        logger.warning(f"Demo data file not found: {DEMO_DATA_PATH}")
        return

    try:
        data = await asyncio.to_thread(_load_json_file, DEMO_DATA_PATH)

        # Build lookup maps for FK resolution
        org_map: dict[str, Organization] = {}
        user_map: dict[str, User] = {}
        project_map: dict[str, Project] = {}
        sprint_map: dict[str, Sprint] = {}  # key: "project_slug:sprint_number"
        agent_type_map: dict[str, AgentType] = {}
        agent_instance_map: dict[
            str, AgentInstance
        ] = {}  # key: "project_slug:agent_name"

        # ========================
        # 1. Create Organizations
        # ========================
        for org_data in data.get("organizations", []):
            org_result = await session.execute(
                select(Organization).where(Organization.slug == org_data["slug"])
            )
            existing_org = org_result.scalar_one_or_none()

            if not existing_org:
                org = Organization(
                    name=org_data["name"],
                    slug=org_data["slug"],
                    description=org_data.get("description"),
                    is_active=True,
                )
                session.add(org)
                await session.flush()
                org_map[str(org.slug)] = org
                logger.info(f"Created demo organization: {org.name}")
            else:
                org_map[str(existing_org.slug)] = existing_org

        # ========================
        # 2. Create Users
        # ========================
        for user_data in data.get("users", []):
            existing_user = await user_crud.get_by_email(
                session, email=user_data["email"]
            )
            if not existing_user:
                user_in = UserCreate(
                    email=user_data["email"],
                    password=user_data["password"],
                    first_name=user_data["first_name"],
                    last_name=user_data["last_name"],
                    is_superuser=user_data["is_superuser"],
                    is_active=user_data.get("is_active", True),
                )
                user = await user_crud.create(session, obj_in=user_in)

                # Randomize created_at for demo data (last 30 days)
                days_ago = random.randint(0, 30)  # noqa: S311
                random_time = datetime.now(UTC) - timedelta(days=days_ago)
                random_time = random_time.replace(
                    hour=random.randint(0, 23),  # noqa: S311
                    minute=random.randint(0, 59),  # noqa: S311
                )

                await session.execute(
                    text(
                        "UPDATE users SET created_at = :created_at, is_active = :is_active WHERE id = :user_id"
                    ),
                    {
                        "created_at": random_time,
                        "is_active": user_data.get("is_active", True),
                        "user_id": user.id,
                    },
                )

                logger.info(
                    f"Created demo user: {user.email} (created {days_ago} days ago)"
                )

                # Add to organization if specified
                org_slug = user_data.get("organization_slug")
                role = user_data.get("role")
                if org_slug and org_slug in org_map and role:
                    org = org_map[org_slug]
                    member = UserOrganization(
                        user_id=user.id, organization_id=org.id, role=role
                    )
                    session.add(member)
                    logger.info(f"Added {user.email} to {org.name} as {role}")

                user_map[str(user.email)] = user
            else:
                user_map[str(existing_user.email)] = existing_user
                logger.debug(f"Demo user already exists: {existing_user.email}")

        await session.flush()

        # Add admin user to map with special "__admin__" key
        # This allows demo data to reference the admin user as owner
        superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com"
        admin_user = await user_crud.get_by_email(session, email=superuser_email)
        if admin_user:
            user_map["__admin__"] = admin_user
            user_map[str(admin_user.email)] = admin_user
            logger.debug(f"Added admin user to map: {admin_user.email}")

        # ========================
        # 3. Load Agent Types Map (for FK resolution)
        # ========================
        agent_types_result = await session.execute(select(AgentType))
        for at in agent_types_result.scalars().all():
            agent_type_map[str(at.slug)] = at

        # ========================
        # 4. Create Projects
        # ========================
        for project_data in data.get("projects", []):
            project_result = await session.execute(
                select(Project).where(Project.slug == project_data["slug"])
            )
            existing_project = project_result.scalar_one_or_none()

            if not existing_project:
                # Resolve owner email to user ID
                owner_id = None
                owner_email = project_data.get("owner_email")
                if owner_email and owner_email in user_map:
                    owner_id = user_map[owner_email].id

                project = Project(
                    name=project_data["name"],
                    slug=project_data["slug"],
                    description=project_data.get("description"),
                    owner_id=owner_id,
                    autonomy_level=AutonomyLevel(
                        project_data.get("autonomy_level", "milestone")
                    ),
                    status=ProjectStatus(project_data.get("status", "active")),
                    complexity=ProjectComplexity(
                        project_data.get("complexity", "medium")
                    ),
                    client_mode=ClientMode(project_data.get("client_mode", "auto")),
                    settings=project_data.get("settings", {}),
                )
                session.add(project)
                await session.flush()
                project_map[str(project.slug)] = project
                logger.info(f"Created demo project: {project.name}")
            else:
                project_map[str(existing_project.slug)] = existing_project
                logger.debug(f"Demo project already exists: {existing_project.name}")

        # ========================
        # 5. Create Sprints
        # ========================
        for sprint_data in data.get("sprints", []):
            project_slug = sprint_data["project_slug"]
            sprint_number = sprint_data["number"]
            sprint_key = f"{project_slug}:{sprint_number}"

            if project_slug not in project_map:
                logger.warning(f"Project not found for sprint: {project_slug}")
                continue

            sprint_project = project_map[project_slug]

            # Check if sprint exists
            sprint_result = await session.execute(
                select(Sprint).where(
                    Sprint.project_id == sprint_project.id,
                    Sprint.number == sprint_number,
                )
            )
            existing_sprint = sprint_result.scalar_one_or_none()

            if not existing_sprint:
                sprint = Sprint(
                    project_id=sprint_project.id,
                    name=sprint_data["name"],
                    number=sprint_number,
                    goal=sprint_data.get("goal"),
                    start_date=date.fromisoformat(sprint_data["start_date"]),
                    end_date=date.fromisoformat(sprint_data["end_date"]),
                    status=SprintStatus(sprint_data.get("status", "planned")),
                    planned_points=sprint_data.get("planned_points"),
                )
                session.add(sprint)
                await session.flush()
                sprint_map[sprint_key] = sprint
                logger.info(
                    f"Created demo sprint: {sprint.name} for {sprint_project.name}"
                )
            else:
                sprint_map[sprint_key] = existing_sprint
                logger.debug(f"Demo sprint already exists: {existing_sprint.name}")

        # ========================
        # 6. Create Agent Instances
        # ========================
        for agent_data in data.get("agent_instances", []):
            project_slug = agent_data["project_slug"]
            agent_type_slug = agent_data["agent_type_slug"]
            agent_name = agent_data["name"]
            agent_key = f"{project_slug}:{agent_name}"

            if project_slug not in project_map:
                logger.warning(f"Project not found for agent: {project_slug}")
                continue

            if agent_type_slug not in agent_type_map:
                logger.warning(f"Agent type not found: {agent_type_slug}")
                continue

            agent_project = project_map[project_slug]
            agent_type = agent_type_map[agent_type_slug]

            # Check if agent instance exists (by name within project)
            agent_result = await session.execute(
                select(AgentInstance).where(
                    AgentInstance.project_id == agent_project.id,
                    AgentInstance.name == agent_name,
                )
            )
            existing_agent = agent_result.scalar_one_or_none()

            if not existing_agent:
                agent_instance = AgentInstance(
                    project_id=agent_project.id,
                    agent_type_id=agent_type.id,
                    name=agent_name,
                    status=AgentStatus(agent_data.get("status", "idle")),
                    current_task=agent_data.get("current_task"),
                )
                session.add(agent_instance)
                await session.flush()
                agent_instance_map[agent_key] = agent_instance
                logger.info(
                    f"Created demo agent: {agent_name} ({agent_type.name}) "
                    f"for {agent_project.name}"
                )
            else:
                agent_instance_map[agent_key] = existing_agent
                logger.debug(f"Demo agent already exists: {existing_agent.name}")

        # ========================
        # 7. Create Issues
        # ========================
        for issue_data in data.get("issues", []):
            project_slug = issue_data["project_slug"]

            if project_slug not in project_map:
                logger.warning(f"Project not found for issue: {project_slug}")
                continue

            issue_project = project_map[project_slug]

            # Check if issue exists (by title within project - simple heuristic)
            issue_result = await session.execute(
                select(Issue).where(
                    Issue.project_id == issue_project.id,
                    Issue.title == issue_data["title"],
                )
            )
            existing_issue = issue_result.scalar_one_or_none()

            if not existing_issue:
                # Resolve sprint
                sprint_id = None
                sprint_number = issue_data.get("sprint_number")
                if sprint_number:
                    sprint_key = f"{project_slug}:{sprint_number}"
                    if sprint_key in sprint_map:
                        sprint_id = sprint_map[sprint_key].id

                # Resolve assigned agent
                assigned_agent_id = None
                assigned_agent_name = issue_data.get("assigned_agent_name")
                if assigned_agent_name:
                    agent_key = f"{project_slug}:{assigned_agent_name}"
                    if agent_key in agent_instance_map:
                        assigned_agent_id = agent_instance_map[agent_key].id

                issue = Issue(
                    project_id=issue_project.id,
                    sprint_id=sprint_id,
                    type=IssueType(issue_data.get("type", "task")),
                    title=issue_data["title"],
                    body=issue_data.get("body", ""),
                    status=IssueStatus(issue_data.get("status", "open")),
                    priority=IssuePriority(issue_data.get("priority", "medium")),
                    labels=issue_data.get("labels", []),
                    story_points=issue_data.get("story_points"),
                    assigned_agent_id=assigned_agent_id,
                )
                session.add(issue)
                logger.info(f"Created demo issue: {issue.title[:50]}...")
            else:
                logger.debug(
                    f"Demo issue already exists: {existing_issue.title[:50]}..."
                )

        await session.commit()
        logger.info("Demo data loaded successfully")

    except Exception as e:
        await session.rollback()
        logger.error(f"Error loading demo data: {e}")
        raise


async def main():
    """Main entry point for database initialization."""
    # Configure logging to show info logs
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    try:
        user = await init_db()
        if user:
            print("Database initialized successfully")
            print(f"Superuser: {user.email}")
        else:
            print("Failed to initialize database")
    except Exception as e:
        print(f"Error initializing database: {e}")
        raise
    finally:
        # Close the engine
        await engine.dispose()


if __name__ == "__main__":
    asyncio.run(main())
