"""Workflow service — CRUD, instance lifecycle, step transitions, event triggers."""

from __future__ import annotations

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

from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.audit import log_audit
from app.models.notification import Notification
from app.models.workflow import Workflow, WorkflowInstance, WorkflowStepHistory


def _safe_iso(dt) -> str | None:
    """Safely convert datetime to ISO string, handling unloaded attributes."""
    if dt is None:
        return None
    try:
        return dt.isoformat() if hasattr(dt, "isoformat") else None
    except Exception:
        return None


def _get_attr(obj, name, default=None):
    """Safely get an attribute that might be expired/unloaded in async context."""
    try:
        val = getattr(obj, name)
        return val if val is not None else default
    except Exception:
        return default


def _workflow_to_dict(w: Workflow) -> dict[str, Any]:
    return {
        "id": str(w.id),
        "name": w.name,
        "description": w.description,
        "trigger_event": w.trigger_event,
        "steps": w.steps,
        "is_active": w.is_active,
        "created_by": str(w.created_by) if w.created_by else None,
        "created_at": _safe_iso(_get_attr(w, "created_at")),
        "updated_at": _safe_iso(_get_attr(w, "updated_at")),
    }


def _instance_to_dict(
    i: WorkflowInstance,
    include_history: bool = False,
    history: list | None = None,
    workflow_name: str | None = None,
) -> dict[str, Any]:
    data = {
        "id": str(i.id),
        "workflow_id": str(i.workflow_id),
        "status": i.status,
        "current_step_index": i.current_step_index,
        "context": i.context,
        "initiated_by": str(i.initiated_by) if i.initiated_by else None,
        "completed_at": _safe_iso(_get_attr(i, "completed_at")),
        "timeout_hours": i.timeout_hours,
        "timeout_at": _safe_iso(_get_attr(i, "timeout_at")),
        "created_at": _safe_iso(_get_attr(i, "created_at")),
        "updated_at": _safe_iso(_get_attr(i, "updated_at")),
    }
    if include_history:
        data["history"] = history or []
        data["workflow_name"] = workflow_name
    return data


def _history_to_dict(h: WorkflowStepHistory) -> dict[str, Any]:
    return {
        "id": str(h.id),
        "instance_id": str(h.instance_id),
        "step_index": h.step_index,
        "step_type": h.step_type,
        "action": h.action,
        "actor_id": str(h.actor_id) if h.actor_id else None,
        "details": h.details,
        "created_at": _safe_iso(_get_attr(h, "created_at")),
    }


async def _log_step_history(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    instance_id: uuid.UUID,
    step_index: int,
    step_type: str,
    action: str,
    actor_id: uuid.UUID | None = None,
    details: dict[str, Any] | None = None,
) -> WorkflowStepHistory:
    """Create a workflow step history entry."""
    entry = WorkflowStepHistory(
        tenant_id=tenant_id,
        instance_id=instance_id,
        step_index=step_index,
        step_type=step_type,
        action=action,
        actor_id=actor_id,
        details=details,
    )
    db.add(entry)
    await db.flush()
    return entry


# ─── Workflow CRUD ───


async def create_workflow(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    data: dict[str, Any],
) -> dict[str, Any]:
    """Create a new workflow definition."""
    steps_raw = data.get("steps", [])
    # Validate steps
    if not steps_raw:
        raise ValueError("Workflow must have at least one step")

    steps_json = [s if isinstance(s, dict) else s.model_dump() for s in steps_raw]

    workflow = Workflow(
        tenant_id=tenant_id,
        name=data["name"],
        description=data.get("description"),
        trigger_event=data.get("trigger_event"),
        steps=steps_json,
        is_active=data.get("is_active", True),
        created_by=user_id,
    )
    db.add(workflow)
    await db.flush()

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="create",
        entity_type="workflow",
        entity_id=workflow.id,
        changes={"name": workflow.name},
    )

    return _workflow_to_dict(workflow)


async def list_workflows(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    page: int = 1,
    page_size: int = 20,
    is_active: bool | None = None,
) -> dict[str, Any]:
    """List workflows with pagination."""
    page = max(1, page)
    page_size = max(1, min(100, page_size))

    base = select(Workflow).where(Workflow.tenant_id == tenant_id)
    if is_active is not None:
        base = base.where(Workflow.is_active == is_active)

    count_q = select(func.count()).select_from(base.subquery())
    total_result = await db.execute(count_q)
    total = total_result.scalar_one()

    offset = (page - 1) * page_size
    paginated = base.order_by(desc(Workflow.created_at)).offset(offset).limit(page_size)
    result = await db.execute(paginated)
    workflows = result.scalars().all()

    return {
        "items": [_workflow_to_dict(w) for w in workflows],
        "total": total,
        "page": page,
        "page_size": page_size,
    }


async def get_workflow(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    workflow_id: str,
) -> dict[str, Any] | None:
    """Get a single workflow by ID."""
    wf_uuid = uuid.UUID(workflow_id)
    result = await db.execute(
        select(Workflow).where(
            Workflow.id == wf_uuid,
            Workflow.tenant_id == tenant_id,
        )
    )
    workflow = result.scalar_one_or_none()
    if workflow is None:
        return None
    return _workflow_to_dict(workflow)


async def update_workflow(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    workflow_id: str,
    data: dict[str, Any],
) -> dict[str, Any] | None:
    """Update a workflow definition."""
    wf_uuid = uuid.UUID(workflow_id)
    result = await db.execute(
        select(Workflow).where(
            Workflow.id == wf_uuid,
            Workflow.tenant_id == tenant_id,
        )
    )
    workflow = result.scalar_one_or_none()
    if workflow is None:
        return None

    if "name" in data:
        workflow.name = data["name"]
    if "description" in data:
        workflow.description = data["description"]
    if "trigger_event" in data:
        workflow.trigger_event = data["trigger_event"]
    if "steps" in data:
        steps_raw = data["steps"]
        workflow.steps = [s if isinstance(s, dict) else s.model_dump() for s in steps_raw]
    if "is_active" in data:
        workflow.is_active = data["is_active"]

    await db.flush()

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="update",
        entity_type="workflow",
        entity_id=workflow.id,
        changes={"updated_fields": list(data.keys())},
    )

    return _workflow_to_dict(workflow)


async def delete_workflow(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    workflow_id: str,
) -> bool:
    """Delete a workflow definition."""
    wf_uuid = uuid.UUID(workflow_id)
    result = await db.execute(
        select(Workflow).where(
            Workflow.id == wf_uuid,
            Workflow.tenant_id == tenant_id,
        )
    )
    workflow = result.scalar_one_or_none()
    if workflow is None:
        return False

    await db.delete(workflow)
    await db.flush()

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="delete",
        entity_type="workflow",
        entity_id=wf_uuid,
    )

    return True


# ─── Instance Lifecycle ───


async def create_instance(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    workflow_id: str,
    context: dict[str, Any] | None = None,
    timeout_hours: int | None = None,
) -> dict[str, Any] | None:
    """Create a new workflow instance with status=pending."""
    wf_uuid = uuid.UUID(workflow_id)
    result = await db.execute(
        select(Workflow).where(
            Workflow.id == wf_uuid,
            Workflow.tenant_id == tenant_id,
        )
    )
    workflow = result.scalar_one_or_none()
    if workflow is None:
        return None

    timeout_at = None
    if timeout_hours:
        timeout_at = datetime.now(UTC) + timedelta(hours=timeout_hours)

    instance = WorkflowInstance(
        tenant_id=tenant_id,
        workflow_id=wf_uuid,
        status="pending",
        current_step_index=0,
        context=context or {},
        initiated_by=user_id,
        timeout_hours=timeout_hours,
        timeout_at=timeout_at,
    )
    db.add(instance)
    await db.flush()
    await db.refresh(instance)

    # Log initial step entry
    steps = workflow.steps or []
    first_step = steps[0] if steps else {}
    await _log_step_history(
        db,
        tenant_id,
        instance.id,
        step_index=0,
        step_type=first_step.get("type", "action"),
        action="entered",
        actor_id=user_id,
        details={"workflow_name": workflow.name},
    )

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="create",
        entity_type="workflow_instance",
        entity_id=instance.id,
        changes={"workflow_id": str(wf_uuid), "status": "pending"},
    )

    return _instance_to_dict(instance)


async def list_instances(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    page: int = 1,
    page_size: int = 20,
    status_filter: str | None = None,
) -> dict[str, Any]:
    """List workflow instances with optional status filter."""
    page = max(1, page)
    page_size = max(1, min(100, page_size))

    base = select(WorkflowInstance).where(WorkflowInstance.tenant_id == tenant_id)
    if status_filter:
        base = base.where(WorkflowInstance.status == status_filter)

    count_q = select(func.count()).select_from(base.subquery())
    total_result = await db.execute(count_q)
    total = total_result.scalar_one()

    offset = (page - 1) * page_size
    paginated = base.order_by(desc(WorkflowInstance.created_at)).offset(offset).limit(page_size)
    result = await db.execute(paginated)
    instances = result.scalars().all()

    return {
        "items": [_instance_to_dict(i) for i in instances],
        "total": total,
        "page": page,
        "page_size": page_size,
    }


async def get_instance(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    instance_id: str,
) -> dict[str, Any] | None:
    """Get a single workflow instance with step history."""
    inst_uuid = uuid.UUID(instance_id)
    result = await db.execute(
        select(WorkflowInstance).where(
            WorkflowInstance.id == inst_uuid,
            WorkflowInstance.tenant_id == tenant_id,
        )
    )
    instance = result.scalar_one_or_none()
    if instance is None:
        return None

    # Get workflow name
    wf_result = await db.execute(select(Workflow.name).where(Workflow.id == instance.workflow_id))
    workflow_name = wf_result.scalar_one_or_none()

    # Get step history
    hist_q = (
        select(WorkflowStepHistory)
        .where(
            WorkflowStepHistory.instance_id == inst_uuid,
            WorkflowStepHistory.tenant_id == tenant_id,
        )
        .order_by(WorkflowStepHistory.created_at)
    )
    hist_result = await db.execute(hist_q)
    history = hist_result.scalars().all()

    return _instance_to_dict(
        instance,
        include_history=True,
        history=[_history_to_dict(h) for h in history],
        workflow_name=workflow_name,
    )


async def advance_instance(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    instance_id: str,
    decision: str,
    comment: str | None = None,
) -> dict[str, Any] | None:
    """Advance or reject a workflow instance step.

    decision: "approve" or "reject"
    - approve: move to next step or complete if last step
    - reject: set status=rejected, notify initiator
    """
    inst_uuid = uuid.UUID(instance_id)
    result = await db.execute(
        select(WorkflowInstance).where(
            WorkflowInstance.id == inst_uuid,
            WorkflowInstance.tenant_id == tenant_id,
        )
    )
    instance = result.scalar_one_or_none()
    if instance is None:
        return None

    if instance.status not in ("pending", "in_progress"):
        return {
            "error": f"Cannot advance instance with status {instance.status}",
            "status_code": 400,
        }

    # Get workflow definition
    wf_result = await db.execute(select(Workflow).where(Workflow.id == instance.workflow_id))
    workflow = wf_result.scalar_one_or_none()
    if workflow is None:
        return {"error": "Workflow definition not found", "status_code": 404}

    steps = workflow.steps or []
    current_idx = instance.current_step_index
    current_step = steps[current_idx] if current_idx < len(steps) else None
    step_type = current_step.get("type", "action") if current_step else "action"

    if decision == "reject":
        instance.status = "rejected"
        instance.completed_at = datetime.now(UTC)
        await _log_step_history(
            db,
            tenant_id,
            instance.id,
            step_index=current_idx,
            step_type=step_type,
            action="rejected",
            actor_id=user_id,
            details={"comment": comment},
        )

        # Notify initiator
        if instance.initiated_by:
            notification = Notification(
                tenant_id=tenant_id,
                user_id=instance.initiated_by,
                type="workflow_rejected",
                title=f"Workflow '{workflow.name}' rejected",
                body=comment or f"Step {current_idx + 1} was rejected",
            )
            db.add(notification)
            await db.flush()

        await log_audit(
            db,
            tenant_id,
            user_id,
            action="reject",
            entity_type="workflow_instance",
            entity_id=instance.id,
            changes={"step": current_idx, "comment": comment},
        )

        return _instance_to_dict(instance)

    # decision == approve
    # Move to in_progress if was pending
    if instance.status == "pending":
        instance.status = "in_progress"

    # Log approval
    await _log_step_history(
        db,
        tenant_id,
        instance.id,
        step_index=current_idx,
        step_type=step_type,
        action="approved",
        actor_id=user_id,
        details={"comment": comment},
    )

    next_idx = current_idx + 1
    if next_idx >= len(steps):
        # Workflow complete
        instance.status = "completed"
        instance.completed_at = datetime.now(UTC)
        await _log_step_history(
            db,
            tenant_id,
            instance.id,
            step_index=current_idx,
            step_type="complete",
            action="completed",
            actor_id=user_id,
        )
    else:
        instance.current_step_index = next_idx
        next_step = steps[next_idx]
        await _log_step_history(
            db,
            tenant_id,
            instance.id,
            step_index=next_idx,
            step_type=next_step.get("type", "action"),
            action="entered",
            actor_id=user_id,
        )

    await db.flush()

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="advance",
        entity_type="workflow_instance",
        entity_id=instance.id,
        changes={
            "decision": decision,
            "step": current_idx,
            "next_step": next_idx if next_idx < len(steps) else None,
        },
    )

    return _instance_to_dict(instance)


async def cancel_instance(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    instance_id: str,
) -> dict[str, Any] | None:
    """Cancel a workflow instance."""
    inst_uuid = uuid.UUID(instance_id)
    result = await db.execute(
        select(WorkflowInstance).where(
            WorkflowInstance.id == inst_uuid,
            WorkflowInstance.tenant_id == tenant_id,
        )
    )
    instance = result.scalar_one_or_none()
    if instance is None:
        return None

    if instance.status in ("completed", "rejected", "cancelled"):
        return {
            "error": f"Cannot cancel instance with status {instance.status}",
            "status_code": 400,
        }

    instance.status = "cancelled"
    instance.completed_at = datetime.now(UTC)

    # Get current step for history
    wf_result = await db.execute(select(Workflow).where(Workflow.id == instance.workflow_id))
    workflow = wf_result.scalar_one_or_none()
    steps = workflow.steps if workflow else []
    current_step = (
        steps[instance.current_step_index] if instance.current_step_index < len(steps) else {}
    )

    await _log_step_history(
        db,
        tenant_id,
        instance.id,
        step_index=instance.current_step_index,
        step_type=current_step.get("type", "action"),
        action="cancelled",
        actor_id=user_id,
    )

    await db.flush()

    await log_audit(
        db,
        tenant_id,
        user_id,
        action="cancel",
        entity_type="workflow_instance",
        entity_id=instance.id,
    )

    return _instance_to_dict(instance)


async def check_timeout(instance: WorkflowInstance) -> bool:
    """Check if an instance has timed out (for approval steps).

    Returns True if the instance should be auto-rejected.
    """
    if instance.timeout_at is None:
        return False
    if instance.status not in ("pending", "in_progress"):
        return False
    return datetime.now(UTC) > instance.timeout_at


async def auto_reject_timeout(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    instance: WorkflowInstance,
) -> dict[str, Any]:
    """Auto-reject a timed-out instance."""
    instance.status = "rejected"
    instance.completed_at = datetime.now(UTC)

    wf_result = await db.execute(select(Workflow).where(Workflow.id == instance.workflow_id))
    workflow = wf_result.scalar_one_or_none()
    steps = workflow.steps if workflow else []
    current_step = (
        steps[instance.current_step_index] if instance.current_step_index < len(steps) else {}
    )

    await _log_step_history(
        db,
        tenant_id,
        instance.id,
        step_index=instance.current_step_index,
        step_type=current_step.get("type", "approval"),
        action="auto_rejected",
        details={"reason": "timeout"},
    )

    # Notify initiator
    if instance.initiated_by:
        notification = Notification(
            tenant_id=tenant_id,
            user_id=instance.initiated_by,
            type="workflow_timeout",
            title=f"Workflow '{workflow.name if workflow else 'Unknown'}' auto-rejected",
            body="The approval step timed out and was automatically rejected.",
        )
        db.add(notification)

    await db.flush()
    await db.refresh(instance)
    return _instance_to_dict(instance)


async def find_workflows_for_event(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    event_name: str,
) -> list[Workflow]:
    """Find active workflows that trigger on a specific event."""
    result = await db.execute(
        select(Workflow).where(
            Workflow.tenant_id == tenant_id,
            Workflow.is_active.is_(True),
            Workflow.trigger_event == event_name,
        )
    )
    return list(result.scalars().all())


async def start_instance_for_event(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID | None,
    event_name: str,
    context: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
    """Start workflow instances for all workflows matching an event trigger.

    Called by the event bus when an event is published.
    """
    workflows = await find_workflows_for_event(db, tenant_id, event_name)
    instances: list[dict[str, Any]] = []
    for wf in workflows:
        inst = await create_instance(
            db,
            tenant_id,
            user_id or uuid.uuid4(),
            str(wf.id),
            context=context,
        )
        if inst:
            instances.append(inst)
    return instances
