"""Service classes for the Automation & Agents plugin.

AgentService, AutomationService, CronJobService, RunLogService.
"""

from __future__ import annotations

import logging
import uuid
from datetime import UTC, datetime
from typing import Any

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

from app.plugins.builtins.automation.models import (
    AgentDefinition,
    AgentRun,
    AgentVersion,
    AutomationCronJob,
    AutomationDefinition,
    AutomationRun,
    AutomationVersion,
)

logger = logging.getLogger(__name__)


# ─── Agent Service ───


class AgentService:
    """CRUD and version management for AI agent definitions."""

    @staticmethod
    async def list(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        is_active: bool | None = None,
        mode: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AgentDefinition], int]:
        """List agent definitions with optional filters."""
        query = select(AgentDefinition).where(AgentDefinition.tenant_id == tenant_id)
        count_query = select(func.count()).select_from(AgentDefinition).where(
            AgentDefinition.tenant_id == tenant_id
        )

        if is_active is not None:
            query = query.where(AgentDefinition.is_active == is_active)
            count_query = count_query.where(AgentDefinition.is_active == is_active)
        if mode is not None:
            query = query.where(AgentDefinition.mode == mode)
            count_query = count_query.where(AgentDefinition.mode == mode)

        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AgentDefinition.created_at.desc())
            .limit(limit)
            .offset(offset)
        )
        items = list(result.scalars().all())
        return items, total

    @staticmethod
    async def get_by_id(
        db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID
    ) -> AgentDefinition | None:
        """Get a single agent definition by ID."""
        result = await db.execute(
            select(AgentDefinition)
            .where(AgentDefinition.id == agent_id)
            .where(AgentDefinition.tenant_id == tenant_id)
            .limit(1)
        )
        return result.scalar_one_or_none()

    @staticmethod
    async def get_by_name(
        db: AsyncSession, name: str, tenant_id: uuid.UUID | None = None
    ) -> AgentDefinition | None:
        """Get a single agent definition by name (optionally scoped to tenant)."""
        query = select(AgentDefinition).where(AgentDefinition.name == name)
        if tenant_id is not None:
            query = query.where(AgentDefinition.tenant_id == tenant_id)
        result = await db.execute(query.limit(1))
        return result.scalar_one_or_none()

    @staticmethod
    async def create(
        db: AsyncSession, tenant_id: uuid.UUID, data: dict[str, Any], user_id: uuid.UUID | None = None
    ) -> AgentDefinition:
        """Create a new agent definition and its initial version snapshot."""
        agent = AgentDefinition(
            tenant_id=tenant_id,
            name=data["name"],
            description=data.get("description", ""),
            llm_model=data.get("llm_model", "ollama/deepseek-v4-flash"),
            system_prompt=data.get("system_prompt", ""),
            tool_ids=data.get("tool_ids", []),
            heartbeat_interval_seconds=data.get("heartbeat_interval_seconds", 0),
            mode=data.get("mode", "reactive"),
            is_active=data.get("is_active", True),
            max_executions_per_hour=data.get("max_executions_per_hour", 10),
            max_duration_seconds=data.get("max_duration_seconds", 300),
            budget_limit_usd=data.get("budget_limit_usd", 1.0),
            created_by=user_id,
        )
        db.add(agent)
        await db.flush()

        # Create initial version snapshot
        version = AgentVersion(
            tenant_id=tenant_id,
            agent_id=agent.id,
            version_number=1,
            snapshot={
                "name": agent.name,
                "description": agent.description,
                "llm_model": agent.llm_model,
                "system_prompt": agent.system_prompt,
                "tool_ids": agent.tool_ids,
                "heartbeat_interval_seconds": agent.heartbeat_interval_seconds,
                "mode": agent.mode,
                "is_active": agent.is_active,
                "max_executions_per_hour": agent.max_executions_per_hour,
                "max_duration_seconds": agent.max_duration_seconds,
                "budget_limit_usd": agent.budget_limit_usd,
            },
            changed_by=user_id,
        )
        db.add(version)
        await db.flush()

        return agent

    @staticmethod
    async def update(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        agent_id: uuid.UUID,
        data: dict[str, Any],
        user_id: uuid.UUID | None = None,
    ) -> AgentDefinition | None:
        """Update an agent definition and create a new version snapshot if changed."""
        agent = await AgentService.get_by_id(db, tenant_id, agent_id)
        if agent is None:
            return None

        changed = False
        for key, value in data.items():
            if hasattr(agent, key) and value is not None:
                setattr(agent, key, value)
                changed = True

        if changed:
            # Get latest version number
            result = await db.execute(
                select(func.coalesce(func.max(AgentVersion.version_number), 0))
                .where(AgentVersion.agent_id == agent_id)
                .where(AgentVersion.tenant_id == tenant_id)
            )
            max_version = result.scalar() or 0

            version = AgentVersion(
                tenant_id=tenant_id,
                agent_id=agent.id,
                version_number=max_version + 1,
                snapshot={
                    "name": agent.name,
                    "description": agent.description,
                    "llm_model": agent.llm_model,
                    "system_prompt": agent.system_prompt,
                    "tool_ids": agent.tool_ids,
                    "heartbeat_interval_seconds": agent.heartbeat_interval_seconds,
                    "mode": agent.mode,
                    "is_active": agent.is_active,
                    "max_executions_per_hour": agent.max_executions_per_hour,
                    "max_duration_seconds": agent.max_duration_seconds,
                    "budget_limit_usd": agent.budget_limit_usd,
                },
                changed_by=user_id,
            )
            db.add(version)
            await db.flush()

        return agent

    @staticmethod
    async def delete(
        db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID
    ) -> bool:
        """Delete an agent definition (cascades to versions and runs)."""
        agent = await AgentService.get_by_id(db, tenant_id, agent_id)
        if agent is None:
            return False
        await db.delete(agent)
        await db.flush()
        return True

    @staticmethod
    async def get_versions(
        db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, limit: int = 50, offset: int = 0
    ) -> tuple[list[AgentVersion], int]:
        """Get version history for an agent definition."""
        query = (
            select(AgentVersion)
            .where(AgentVersion.agent_id == agent_id)
            .where(AgentVersion.tenant_id == tenant_id)
        )
        count_query = (
            select(func.count())
            .select_from(AgentVersion)
            .where(AgentVersion.agent_id == agent_id)
            .where(AgentVersion.tenant_id == tenant_id)
        )
        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AgentVersion.version_number.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total

    @staticmethod
    async def restore_version(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        agent_id: uuid.UUID,
        version_id: uuid.UUID,
        user_id: uuid.UUID | None = None,
    ) -> AgentDefinition | None:
        """Restore an agent definition from a specific version snapshot."""
        agent = await AgentService.get_by_id(db, tenant_id, agent_id)
        if agent is None:
            return None

        result = await db.execute(
            select(AgentVersion)
            .where(AgentVersion.id == version_id)
            .where(AgentVersion.agent_id == agent_id)
            .where(AgentVersion.tenant_id == tenant_id)
            .limit(1)
        )
        version = result.scalar_one_or_none()
        if version is None:
            return None

        snapshot = version.snapshot
        for key, value in snapshot.items():
            if hasattr(agent, key):
                setattr(agent, key, value)

        # Create new version reflecting the restore
        max_ver_result = await db.execute(
            select(func.coalesce(func.max(AgentVersion.version_number), 0))
            .where(AgentVersion.agent_id == agent_id)
            .where(AgentVersion.tenant_id == tenant_id)
        )
        max_version = max_ver_result.scalar() or 0

        new_version = AgentVersion(
            tenant_id=tenant_id,
            agent_id=agent.id,
            version_number=max_version + 1,
            snapshot=snapshot,
            changed_by=user_id,
        )
        db.add(new_version)
        await db.flush()

        return agent


# ─── Automation Service ───


class AutomationService:
    """CRUD and version management for automation definitions."""

    @staticmethod
    async def list(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        trigger_type: str | None = None,
        is_active: bool | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AutomationDefinition], int]:
        """List automation definitions with optional filters."""
        query = select(AutomationDefinition).where(
            AutomationDefinition.tenant_id == tenant_id
        )
        count_query = (
            select(func.count())
            .select_from(AutomationDefinition)
            .where(AutomationDefinition.tenant_id == tenant_id)
        )

        if trigger_type is not None:
            query = query.where(AutomationDefinition.trigger_type == trigger_type)
            count_query = count_query.where(
                AutomationDefinition.trigger_type == trigger_type
            )
        if is_active is not None:
            query = query.where(AutomationDefinition.is_active == is_active)
            count_query = count_query.where(
                AutomationDefinition.is_active == is_active
            )

        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AutomationDefinition.created_at.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total

    @staticmethod
    async def get_by_id(
        db: AsyncSession, tenant_id: uuid.UUID, automation_id: uuid.UUID
    ) -> AutomationDefinition | None:
        """Get a single automation definition by ID."""
        result = await db.execute(
            select(AutomationDefinition)
            .where(AutomationDefinition.id == automation_id)
            .where(AutomationDefinition.tenant_id == tenant_id)
            .limit(1)
        )
        return result.scalar_one_or_none()

    @staticmethod
    async def get_by_name(
        db: AsyncSession, name: str, tenant_id: uuid.UUID | None = None
    ) -> AutomationDefinition | None:
        """Get a single automation definition by name (optionally scoped to tenant)."""
        query = select(AutomationDefinition).where(AutomationDefinition.name == name)
        if tenant_id is not None:
            query = query.where(AutomationDefinition.tenant_id == tenant_id)
        result = await db.execute(query.limit(1))
        return result.scalar_one_or_none()

    @staticmethod
    async def create(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        data: dict[str, Any],
        user_id: uuid.UUID | None = None,
    ) -> AutomationDefinition:
        """Create a new automation definition and its initial version snapshot."""
        automation = AutomationDefinition(
            tenant_id=tenant_id,
            name=data["name"],
            description=data.get("description", ""),
            trigger_type=data.get("trigger_type", "manual"),
            trigger_config=data.get("trigger_config", {}),
            conditions=data.get("conditions", []),
            actions=data.get("actions", []),
            is_active=data.get("is_active", True),
            dry_run=data.get("dry_run", False),
            created_by=user_id,
        )
        db.add(automation)
        await db.flush()

        # Create initial version snapshot
        version = AutomationVersion(
            tenant_id=tenant_id,
            automation_id=automation.id,
            version_number=1,
            snapshot={
                "name": automation.name,
                "description": automation.description,
                "trigger_type": automation.trigger_type,
                "trigger_config": automation.trigger_config,
                "conditions": automation.conditions,
                "actions": automation.actions,
                "is_active": automation.is_active,
                "dry_run": automation.dry_run,
            },
            changed_by=user_id,
        )
        db.add(version)
        await db.flush()

        return automation

    @staticmethod
    async def update(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        automation_id: uuid.UUID,
        data: dict[str, Any],
        user_id: uuid.UUID | None = None,
    ) -> AutomationDefinition | None:
        """Update an automation definition and create a new version snapshot if changed."""
        automation = await AutomationService.get_by_id(db, tenant_id, automation_id)
        if automation is None:
            return None

        changed = False
        for key, value in data.items():
            if hasattr(automation, key) and value is not None:
                setattr(automation, key, value)
                changed = True

        if changed:
            result = await db.execute(
                select(func.coalesce(func.max(AutomationVersion.version_number), 0))
                .where(AutomationVersion.automation_id == automation_id)
                .where(AutomationVersion.tenant_id == tenant_id)
            )
            max_version = result.scalar() or 0

            version = AutomationVersion(
                tenant_id=tenant_id,
                automation_id=automation.id,
                version_number=max_version + 1,
                snapshot={
                    "name": automation.name,
                    "description": automation.description,
                    "trigger_type": automation.trigger_type,
                    "trigger_config": automation.trigger_config,
                    "conditions": automation.conditions,
                    "actions": automation.actions,
                    "is_active": automation.is_active,
                    "dry_run": automation.dry_run,
                },
                changed_by=user_id,
            )
            db.add(version)
            await db.flush()

        return automation

    @staticmethod
    async def delete(
        db: AsyncSession, tenant_id: uuid.UUID, automation_id: uuid.UUID
    ) -> bool:
        """Delete an automation definition (cascades to versions and runs)."""
        automation = await AutomationService.get_by_id(db, tenant_id, automation_id)
        if automation is None:
            return False
        await db.delete(automation)
        await db.flush()
        return True

    @staticmethod
    async def get_versions(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        automation_id: uuid.UUID,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AutomationVersion], int]:
        """Get version history for an automation definition."""
        query = (
            select(AutomationVersion)
            .where(AutomationVersion.automation_id == automation_id)
            .where(AutomationVersion.tenant_id == tenant_id)
        )
        count_query = (
            select(func.count())
            .select_from(AutomationVersion)
            .where(AutomationVersion.automation_id == automation_id)
            .where(AutomationVersion.tenant_id == tenant_id)
        )
        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AutomationVersion.version_number.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total

    @staticmethod
    async def restore_version(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        automation_id: uuid.UUID,
        version_id: uuid.UUID,
        user_id: uuid.UUID | None = None,
    ) -> AutomationDefinition | None:
        """Restore an automation definition from a specific version snapshot."""
        automation = await AutomationService.get_by_id(db, tenant_id, automation_id)
        if automation is None:
            return None

        result = await db.execute(
            select(AutomationVersion)
            .where(AutomationVersion.id == version_id)
            .where(AutomationVersion.automation_id == automation_id)
            .where(AutomationVersion.tenant_id == tenant_id)
            .limit(1)
        )
        version = result.scalar_one_or_none()
        if version is None:
            return None

        snapshot = version.snapshot
        for key, value in snapshot.items():
            if hasattr(automation, key):
                setattr(automation, key, value)

        max_ver_result = await db.execute(
            select(func.coalesce(func.max(AutomationVersion.version_number), 0))
            .where(AutomationVersion.automation_id == automation_id)
            .where(AutomationVersion.tenant_id == tenant_id)
        )
        max_version = max_ver_result.scalar() or 0

        new_version = AutomationVersion(
            tenant_id=tenant_id,
            automation_id=automation.id,
            version_number=max_version + 1,
            snapshot=snapshot,
            changed_by=user_id,
        )
        db.add(new_version)
        await db.flush()

        return automation


# ─── Cron Job Service ───


class CronJobService:
    """CRUD and next_run calculation for cron job schedules."""

    @staticmethod
    async def list(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        job_type: str | None = None,
        is_active: bool | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AutomationCronJob], int]:
        """List cron jobs with optional filters."""
        query = select(AutomationCronJob).where(
            AutomationCronJob.tenant_id == tenant_id
        )
        count_query = (
            select(func.count())
            .select_from(AutomationCronJob)
            .where(AutomationCronJob.tenant_id == tenant_id)
        )

        if job_type is not None:
            query = query.where(AutomationCronJob.job_type == job_type)
            count_query = count_query.where(AutomationCronJob.job_type == job_type)
        if is_active is not None:
            query = query.where(AutomationCronJob.is_active == is_active)
            count_query = count_query.where(AutomationCronJob.is_active == is_active)

        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AutomationCronJob.created_at.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total

    @staticmethod
    async def get_by_id(
        db: AsyncSession, tenant_id: uuid.UUID, job_id: uuid.UUID
    ) -> AutomationCronJob | None:
        """Get a single cron job by ID."""
        result = await db.execute(
            select(AutomationCronJob)
            .where(AutomationCronJob.id == job_id)
            .where(AutomationCronJob.tenant_id == tenant_id)
            .limit(1)
        )
        return result.scalar_one_or_none()

    @staticmethod
    async def get_by_name(
        db: AsyncSession, name: str, tenant_id: uuid.UUID | None = None
    ) -> AutomationCronJob | None:
        """Get a single cron job by name (optionally scoped to tenant)."""
        query = select(AutomationCronJob).where(AutomationCronJob.name == name)
        if tenant_id is not None:
            query = query.where(AutomationCronJob.tenant_id == tenant_id)
        result = await db.execute(query.limit(1))
        return result.scalar_one_or_none()

    @staticmethod
    async def create(
        db: AsyncSession, tenant_id: uuid.UUID, data: dict[str, Any]
    ) -> AutomationCronJob:
        """Create a new cron job entry."""
        job = AutomationCronJob(
            tenant_id=tenant_id,
            name=data["name"],
            cron_expression=data["cron_expression"],
            job_type=data.get("job_type", "automation_trigger"),
            target_id=data.get("target_id"),
            plugin_name=data.get("plugin_name", ""),
            is_active=data.get("is_active", True),
        )
        db.add(job)
        await db.flush()
        return job

    @staticmethod
    async def update(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        job_id: uuid.UUID,
        data: dict[str, Any],
    ) -> AutomationCronJob | None:
        """Update a cron job entry."""
        job = await CronJobService.get_by_id(db, tenant_id, job_id)
        if job is None:
            return None

        for key, value in data.items():
            if hasattr(job, key) and value is not None:
                setattr(job, key, value)
        await db.flush()
        return job

    @staticmethod
    async def delete(
        db: AsyncSession, tenant_id: uuid.UUID, job_id: uuid.UUID
    ) -> bool:
        """Delete a cron job entry."""
        job = await CronJobService.get_by_id(db, tenant_id, job_id)
        if job is None:
            return False
        await db.delete(job)
        await db.flush()
        return True

    @staticmethod
    async def get_due_jobs(
        db: AsyncSession, limit: int = 100
    ) -> list[AutomationCronJob]:
        """Get cron jobs that are due for execution (next_run_at <= now)."""
        now = datetime.now(UTC)
        result = await db.execute(
            select(AutomationCronJob)
            .where(AutomationCronJob.is_active == True)
            .where(AutomationCronJob.next_run_at <= now)
            .limit(limit)
        )
        return list(result.scalars().all())


# ─── Run Log Service ───


class RunLogService:
    """Query execution logs for agents and automations."""

    @staticmethod
    async def list_agent_runs(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        agent_id: uuid.UUID | None = None,
        status: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AgentRun], int]:
        """List agent run logs with optional filters."""
        query = select(AgentRun).where(AgentRun.tenant_id == tenant_id)
        count_query = (
            select(func.count())
            .select_from(AgentRun)
            .where(AgentRun.tenant_id == tenant_id)
        )

        if agent_id is not None:
            query = query.where(AgentRun.agent_id == agent_id)
            count_query = count_query.where(AgentRun.agent_id == agent_id)
        if status is not None:
            query = query.where(AgentRun.status == status)
            count_query = count_query.where(AgentRun.status == status)

        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AgentRun.started_at.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total

    @staticmethod
    async def list_automation_runs(
        db: AsyncSession,
        tenant_id: uuid.UUID,
        automation_id: uuid.UUID | None = None,
        status: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[AutomationRun], int]:
        """List automation run logs with optional filters."""
        query = select(AutomationRun).where(AutomationRun.tenant_id == tenant_id)
        count_query = (
            select(func.count())
            .select_from(AutomationRun)
            .where(AutomationRun.tenant_id == tenant_id)
        )

        if automation_id is not None:
            query = query.where(AutomationRun.automation_id == automation_id)
            count_query = count_query.where(
                AutomationRun.automation_id == automation_id
            )
        if status is not None:
            query = query.where(AutomationRun.status == status)
            count_query = count_query.where(AutomationRun.status == status)

        count_result = await db.execute(count_query)
        total = count_result.scalar() or 0

        result = await db.execute(
            query.order_by(AutomationRun.started_at.desc())
            .limit(limit)
            .offset(offset)
        )
        return list(result.scalars().all()), total
