from __future__ import annotations

import uuid
from collections.abc import Sequence
from typing import Literal

import sqlalchemy as sa
from sqlalchemy import and_, cast, func, or_, select
from sqlalchemy.orm import selectinload

from tracecat.authz.controls import require_scope
from tracecat.db.models import (
    CaseTrigger,
    Schedule,
    Webhook,
    Workflow,
    WorkflowDefinition,
    WorkflowFolder,
)
from tracecat.exceptions import TracecatNotFoundError, TracecatValidationError
from tracecat.identifiers import WorkflowID
from tracecat.identifiers.workflow import WorkflowUUID
from tracecat.service import BaseWorkspaceService
from tracecat.tags.schemas import TagRead
from tracecat.workflow.management.folders.schemas import (
    DirectoryItem,
    FolderDirectoryItem,
    WorkflowDirectoryItem,
)
from tracecat.workflow.management.schemas import (
    WorkflowDefinitionReadMinimal,
    WorkflowTriggerSummary,
)
from tracecat.workflow.management.types import build_workflow_trigger_summary


class WorkflowFolderService(BaseWorkspaceService):
    """Service for managing workflow folders using materialized path pattern."""

    service_name = "workflow_folders"

    @staticmethod
    def _normalize_folder_path(path: str) -> str:
        """Normalize folder paths to materialized-path format."""
        if path == "/":
            return path
        return path if path.endswith("/") else f"{path}/"

    @require_scope("workflow:create")
    async def create_folder(
        self, name: str, parent_path: str = "/", commit: bool = True
    ) -> WorkflowFolder:
        """Create a new workflow folder.

        Args:
            name: The name of the folder
            parent_path: Path of the parent folder, defaults to root

        Returns:
            The created folder
        """
        # Validate name - no slashes allowed
        if "/" in name:
            raise TracecatValidationError("Folder name cannot contain slashes")

        parent_path = self._normalize_folder_path(parent_path)

        # Ensure parent path exists if not root
        if parent_path != "/":
            # We want to create the nested folders if not
            parent_exists = await self._folder_path_exists(parent_path)
            if not parent_exists:
                raise TracecatValidationError(f"Parent path {parent_path} not found")

        # Create full path
        full_path = f"{parent_path}{name}/" if parent_path != "/" else f"/{name}/"

        # Check if path already exists
        path_exists = await self._folder_path_exists(full_path)
        if path_exists:
            raise TracecatValidationError(f"Folder {full_path} already exists")

        folder = WorkflowFolder(
            name=name,
            path=full_path,
            workspace_id=self.workspace_id,
        )
        self.session.add(folder)
        if commit:
            await self.session.commit()
        else:
            await self.session.flush()
        await self.session.refresh(folder)
        return folder

    async def get_folder(self, folder_id: uuid.UUID) -> WorkflowFolder | None:
        """Get a folder by ID.

        Args:
            folder_id: The ID of the folder

        Returns:
            The folder or None if not found
        """
        statement = select(WorkflowFolder).where(
            WorkflowFolder.workspace_id == self.workspace_id,
            WorkflowFolder.id == folder_id,
        )
        result = await self.session.execute(statement)
        return result.scalar_one_or_none()

    async def get_folder_by_path(self, path: str) -> WorkflowFolder | None:
        """Get a folder by its path.

        Args:
            path: The path of the folder

        Returns:
            The folder or None if not found
        """
        # Ensure path has trailing slash
        if not path.endswith("/") and path != "/":
            path += "/"

        statement = select(WorkflowFolder).where(
            WorkflowFolder.workspace_id == self.workspace_id,
            WorkflowFolder.path == path,
        )
        result = await self.session.execute(statement)
        return result.scalar_one_or_none()

    async def list_folders(self, parent_path: str = "/") -> Sequence[WorkflowFolder]:
        """List all folders within the specified parent path subtree, or all folders if no path is specified.

        Args:
            parent_path: Optional path of the parent folder. If None or "/", lists all folders.
                         Otherwise, lists all folders whose path starts with parent_path.

        Returns:
            List of folders within the specified subtree or all folders.
        """
        # Base statement selecting folders for the current workspace

        statement = select(WorkflowFolder).where(
            WorkflowFolder.workspace_id == self.workspace_id,
            WorkflowFolder.path.like(f"{parent_path}%"),
        )

        # Execute the query and return all matching folders
        result = await self.session.execute(statement)
        return result.scalars().all()

    async def get_workflows_in_folder(
        self, folder_id: uuid.UUID | None = None
    ) -> Sequence[Workflow]:
        """Get workflows in the specified folder.

        If folder_id is None, returns workflows not in any folder.

        Args:
            folder_id: Optional ID of the folder

        Returns:
            List of workflows in the folder
        """
        statement = select(Workflow).where(
            Workflow.workspace_id == self.workspace_id,
            Workflow.folder_id == folder_id,
        )
        result = await self.session.execute(statement)
        return result.scalars().all()

    @require_scope("workflow:update")
    async def move_workflow(
        self, workflow_id: WorkflowID, folder: WorkflowFolder | None = None
    ) -> Workflow:
        """Move a workflow to a different folder.

        Args:
            workflow_id: ID of the workflow to move
            folder_id: ID of the destination folder

        Returns:
            The updated workflow
        """
        # Update the workflow
        statement = select(Workflow).where(
            Workflow.workspace_id == self.workspace_id,
            Workflow.id == workflow_id,
        )
        result = await self.session.execute(statement)
        workflow = result.scalar_one_or_none()
        if not workflow:
            raise TracecatNotFoundError(f"Workflow {workflow_id} not found")

        workflow.folder_id = folder.id if folder else None

        self.session.add(workflow)
        await self.session.commit()
        await self.session.refresh(workflow)
        return workflow

    @require_scope("workflow:update")
    async def rename_folder(
        self, folder_id: uuid.UUID, new_name: str
    ) -> WorkflowFolder:
        """Rename a folder. Updates the folder name and path.

        Args:
            folder_id: ID of the folder to rename
            new_name: New name for the folder

        Returns:
            The updated folder
        """
        # Validate name - no slashes allowed
        if "/" in new_name:
            raise TracecatValidationError("Folder name cannot contain slashes")

        folder = await self.get_folder(folder_id)
        if not folder:
            raise TracecatValidationError(f"Folder {folder_id} not found")

        old_path = folder.path
        parent_path = folder.parent_path

        # Create the new path
        new_path = (
            f"{parent_path}{new_name}/" if parent_path != "/" else f"/{new_name}/"
        )

        # Check if new path already exists
        if new_path != old_path:
            path_exists = await self._folder_path_exists(new_path)
            if not path_exists:
                # TODO: Create the folder
                pass
            # if path_exists:
            #     raise TracecatValidationError(f"Folder {new_path} already exists")

        # Get all descendants to update their paths
        descendants = await self._get_descendants(old_path)

        # Update this folder
        folder.name = new_name
        folder.path = new_path
        self.session.add(folder)

        # Update all descendant paths
        for descendant in descendants:
            # Replace the old path prefix with the new one
            descendant.path = descendant.path.replace(old_path, new_path, 1)
            self.session.add(descendant)

        await self.session.commit()
        await self.session.refresh(folder)
        return folder

    @require_scope("workflow:update")
    async def move_folder(
        self, folder_id: uuid.UUID, new_parent_id: uuid.UUID | None
    ) -> WorkflowFolder:
        """Move a folder to a different parent.

        Args:
            folder_id: ID of the folder to move
            new_parent_id: ID of the new parent folder, or None for root

        Returns:
            The updated folder
        """
        folder = await self.get_folder(folder_id)
        if not folder:
            raise TracecatValidationError(f"Folder {folder_id} not found")

        # Determine new parent path
        new_parent_path = "/"
        if new_parent_id is not None:
            new_parent = await self.get_folder(new_parent_id)
            if not new_parent:
                raise TracecatValidationError(
                    f"Parent folder {new_parent_id} not found"
                )
            new_parent_path = new_parent.path

            # Check if we're trying to make a folder its own descendant
            if folder.path == new_parent_path:
                raise TracecatValidationError("Cannot make a folder its own child")

            # Check if new parent is a descendant of the folder (would create a cycle)
            if new_parent.path.startswith(folder.path):
                raise TracecatValidationError("Cannot create cyclic folder structure")

        old_path = folder.path
        old_name = folder.name

        # Create the new path
        new_path = (
            f"{new_parent_path}{old_name}/"
            if new_parent_path != "/"
            else f"/{old_name}/"
        )

        # Check if new path already exists
        if new_path != old_path:
            path_exists = await self._folder_path_exists(new_path)
            if path_exists:
                raise TracecatValidationError(f"Folder {new_path} already exists")

        # Get all descendants to update their paths
        descendants = await self._get_descendants(old_path)

        # Update this folder
        folder.path = new_path
        self.session.add(folder)

        # Update all descendant paths
        for descendant in descendants:
            # Replace the old path prefix with the new one
            descendant.path = descendant.path.replace(old_path, new_path, 1)
            self.session.add(descendant)

        await self.session.commit()
        await self.session.refresh(folder)
        return folder

    @require_scope("workflow:delete")
    async def delete_folder(
        self, folder_id: uuid.UUID, recursive: bool = False
    ) -> None:
        """Delete a folder.

        Args:
            folder_id: ID of the folder to delete
            recursive: If True, also delete subfolders and contained workflows
                      If False, will fail if folder has children or workflows
        """
        folder = await self.get_folder(folder_id)
        if not folder:
            raise TracecatValidationError(f"Folder {folder_id} not found")

        # Prevent deletion of root folder
        if folder.path == "/":
            raise TracecatValidationError("Cannot delete root folder")

        if not recursive:
            # Check if folder has children or workflows
            has_children = await self._has_children(folder.path)
            has_workflows = await self._has_workflows(folder_id)

            if has_children or has_workflows:
                raise TracecatValidationError(
                    "Folder is not empty. Please move or delete its contents first."
                )
        else:
            # If recursive, delete all subfolders first
            descendants = await self._get_descendants(folder.path)
            for descendant in descendants:
                # Delete workflows in each subfolder
                statement = select(Workflow).where(
                    Workflow.workspace_id == self.workspace_id,
                    Workflow.folder_id == descendant.id,
                )
                result = await self.session.execute(statement)
                workflows = result.scalars().all()
                for workflow in workflows:
                    workflow.folder_id = None
                    self.session.add(workflow)

                # Delete the subfolder
                await self.session.delete(descendant)

            # Delete workflows in the main folder
            statement = select(Workflow).where(
                Workflow.workspace_id == self.workspace_id,
                Workflow.folder_id == folder.id,
            )
            result = await self.session.execute(statement)
            workflows = result.scalars().all()
            for workflow in workflows:
                workflow.folder_id = None
                self.session.add(workflow)

        # Delete the folder
        await self.session.delete(folder)
        await self.session.commit()

    async def get_folder_tree(self, root_path: str = "/") -> Sequence[WorkflowFolder]:
        """Get the full folder tree starting from the given root path.

        Args:
            root_path: Path of the root folder, defaults to "/"

        Returns:
            List of folders in the tree
        """
        # Ensure root_path has trailing slash
        if not root_path.endswith("/") and root_path != "/":
            root_path += "/"

        statement = (
            select(WorkflowFolder)
            .where(
                WorkflowFolder.workspace_id == self.workspace_id,
                or_(
                    WorkflowFolder.path.startswith(root_path),
                    WorkflowFolder.path == root_path,
                ),
            )
            .order_by(WorkflowFolder.path)
        )

        result = await self.session.execute(statement)
        return result.scalars().all()

    async def _folder_path_exists(self, path: str) -> bool:
        """Check if a folder path exists."""
        path = self._normalize_folder_path(path)
        statement = (
            select(func.count())
            .select_from(WorkflowFolder)
            .where(
                WorkflowFolder.workspace_id == self.workspace_id,
                WorkflowFolder.path == path,
            )
        )
        result = await self.session.execute(statement)
        return result.scalar_one() > 0

    async def _has_children(self, path: str) -> bool:
        """Check if a folder has any child folders."""
        # Ensure path has trailing slash
        if not path.endswith("/") and path != "/":
            path += "/"

        statement = (
            select(func.count())
            .select_from(WorkflowFolder)
            .where(
                WorkflowFolder.workspace_id == self.workspace_id,
                WorkflowFolder.path.startswith(path),
                WorkflowFolder.path != path,
            )
        )
        result = await self.session.execute(statement)
        return result.scalar_one() > 0

    async def _has_workflows(self, folder_id: uuid.UUID) -> bool:
        """Check if a folder contains any workflows."""
        statement = (
            select(func.count())
            .select_from(Workflow)
            .where(
                Workflow.workspace_id == self.workspace_id,
                Workflow.folder_id == folder_id,
            )
        )
        result = await self.session.execute(statement)
        return result.scalar_one() > 0

    async def _get_descendants(self, path: str) -> Sequence[WorkflowFolder]:
        """Get all descendant folders of a given path."""
        # Ensure path has trailing slash
        if not path.endswith("/") and path != "/":
            path += "/"

        statement = select(WorkflowFolder).where(
            WorkflowFolder.workspace_id == self.workspace_id,
            WorkflowFolder.path.startswith(path),
            WorkflowFolder.path != path,
        )
        result = await self.session.execute(statement)
        return result.scalars().all()

    async def get_directory_items(
        self, path: str = "/", *, order_by: Literal["asc", "desc"] = "desc"
    ) -> Sequence[DirectoryItem]:
        """Get all directory items (workflows and folders) in the given path.

        Args:
            path: The folder path to list items from. Use "/" for root level items.

        Returns:
            Sequence of DirectoryItems (workflows and folders) in the path
        """
        # Ensure path has trailing slash
        if not path.endswith("/") and path != "/":
            path += "/"

        # Subquery to get the latest definition for each workflow
        latest_defn_subq = (
            select(
                WorkflowDefinition.workflow_id,
                sa.func.max(WorkflowDefinition.version).label("latest_version"),
            )
            .group_by(cast(WorkflowDefinition.workflow_id, sa.UUID))
            .subquery()
        )

        schedule_count_subq = (
            select(
                Schedule.workflow_id.label("workflow_id"),
                sa.func.count(Schedule.id).label("online_schedule_count"),
            )
            .where(Schedule.status == "online")
            .group_by(Schedule.workflow_id)
            .subquery()
        )
        ranked_schedule_subq = select(
            Schedule.workflow_id.label("workflow_id"),
            Schedule.cron.label("schedule_cron"),
            Schedule.every.label("schedule_every"),
            sa.func.row_number()
            .over(
                partition_by=Schedule.workflow_id,
                order_by=(
                    sa.case((Schedule.status == "online", 0), else_=1).asc(),
                    Schedule.updated_at.desc(),
                    Schedule.id.desc(),
                ),
            )
            .label("row_num"),
        ).subquery()
        schedule_preview_subq = (
            select(
                ranked_schedule_subq.c.workflow_id,
                ranked_schedule_subq.c.schedule_cron,
                ranked_schedule_subq.c.schedule_every,
            )
            .where(ranked_schedule_subq.c.row_num == 1)
            .subquery()
        )
        webhook_summary_subq = (
            select(
                Webhook.workflow_id.label("workflow_id"),
                sa.func.bool_or(Webhook.status == "online").label("webhook_active"),
            )
            .group_by(Webhook.workflow_id)
            .subquery()
        )
        if path != "/":
            folder = await self.get_folder_by_path(path)
            if not folder:
                raise TracecatNotFoundError(f"Folder {path} not found")
            folder_id = folder.id
        else:
            folder_id = None

        workflow_statement = (
            select(
                Workflow,
                WorkflowDefinition.id,
                WorkflowDefinition.version,
                WorkflowDefinition.created_at,
                webhook_summary_subq.c.webhook_active,
                CaseTrigger.event_types.label("case_trigger_event_types"),
                schedule_count_subq.c.online_schedule_count,
                schedule_preview_subq.c.schedule_cron,
                schedule_preview_subq.c.schedule_every,
            )
            .where(
                Workflow.workspace_id == self.workspace_id,
                Workflow.folder_id == folder_id,
            )
            .outerjoin(
                latest_defn_subq,
                cast(Workflow.id, sa.UUID) == latest_defn_subq.c.workflow_id,
            )
            .outerjoin(
                WorkflowDefinition,
                and_(
                    WorkflowDefinition.workflow_id == Workflow.id,
                    WorkflowDefinition.version == latest_defn_subq.c.latest_version,
                ),
            )
            .outerjoin(
                CaseTrigger,
                and_(
                    cast(Workflow.id, sa.UUID) == CaseTrigger.workflow_id,
                    CaseTrigger.status == "online",
                ),
            )
            .outerjoin(
                webhook_summary_subq,
                cast(Workflow.id, sa.UUID) == webhook_summary_subq.c.workflow_id,
            )
            .outerjoin(
                schedule_count_subq,
                cast(Workflow.id, sa.UUID) == schedule_count_subq.c.workflow_id,
            )
            .outerjoin(
                schedule_preview_subq,
                cast(Workflow.id, sa.UUID) == schedule_preview_subq.c.workflow_id,
            )
            .order_by(
                Workflow.created_at.desc()
                if order_by == "desc"
                else Workflow.created_at.asc()
            )
            .options(selectinload(Workflow.tags))
        )

        workflow_result = await self.session.execute(workflow_statement)
        workflows_with_defns = workflow_result.tuples().all()
        # For root path, get workflows with no folder_id
        if path == "/":
            # Get root-level folders
            folder_statement = select(WorkflowFolder).where(
                WorkflowFolder.workspace_id == self.workspace_id,
                func.length(WorkflowFolder.path)
                - func.length(func.replace(WorkflowFolder.path, "/", ""))
                == 2,  # folders with exactly two slashes
            )
            folder_result = await self.session.execute(folder_statement)
            folders = folder_result.scalars().all()
        else:
            # Get direct child folders
            folder_statement = select(WorkflowFolder).where(
                WorkflowFolder.workspace_id == self.workspace_id,
                WorkflowFolder.path.startswith(path),
                WorkflowFolder.path != path,
                ~WorkflowFolder.path.like(f"{path}%/%/"),  # Exclude nested folders
            )
            folder_result = await self.session.execute(folder_statement)
            folders = folder_result.scalars().all()

        # Convert to directory items
        directory_items: list[DirectoryItem] = []

        # Add folders with item counts
        for folder in folders:
            has_children = await self._has_children(folder.path)
            has_workflows = await self._has_workflows(folder.id)
            num_items = (1 if has_children else 0) + (1 if has_workflows else 0)

            directory_items.append(
                FolderDirectoryItem(
                    type="folder",
                    num_items=num_items,
                    id=folder.id,
                    name=folder.name,
                    path=folder.path,
                    workspace_id=folder.workspace_id,
                    created_at=folder.created_at,
                    updated_at=folder.updated_at,
                )
            )

        # Add workflows
        for (
            workflow,
            defn_id,
            defn_version,
            defn_created,
            webhook_active,
            case_trigger_event_types,
            online_schedule_count,
            schedule_cron,
            schedule_every,
        ) in workflows_with_defns:
            if all((defn_id, defn_version, defn_created)):
                latest_definition = WorkflowDefinitionReadMinimal(
                    id=defn_id,
                    version=defn_version,
                    created_at=defn_created,
                )
            else:
                latest_definition = None
            trigger_summary = build_workflow_trigger_summary(
                online_schedule_count=online_schedule_count,
                schedule_cron=schedule_cron,
                schedule_every=schedule_every,
                webhook_active=webhook_active,
                case_trigger_event_types=case_trigger_event_types,
            )
            directory_items.append(
                WorkflowDirectoryItem(
                    type="workflow",
                    id=WorkflowUUID.new(workflow.id).short(),
                    alias=workflow.alias,
                    created_at=workflow.created_at,
                    updated_at=workflow.updated_at,
                    version=workflow.version,
                    title=workflow.title,
                    description=workflow.description,
                    status=workflow.status,
                    latest_definition=latest_definition,
                    icon_url=workflow.icon_url,
                    tags=[
                        TagRead.model_validate(tag, from_attributes=True)
                        for tag in workflow.tags
                    ],
                    trigger_summary=(
                        WorkflowTriggerSummary.model_validate(
                            trigger_summary, from_attributes=True
                        )
                        if trigger_summary
                        else None
                    ),
                )
            )

        return directory_items
