import copy
import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission
from app.core.security import AuthContext, get_auth_context
from app.crud import workflow_engine
from app.models.workflow import Form, Workflow, WorkflowStepForm
from app.schemas.workflow import (
    WorkflowCreate,
    WorkflowDuplicate,
    WorkflowRead,
    WorkflowStepFormCreate,
    WorkflowStepFormRead,
    WorkflowUpdate,
)

router = APIRouter(prefix="/workflows", tags=["workflows"])


async def _get_current(db: AsyncSession, tenant_id: uuid.UUID, workflow_id: uuid.UUID) -> Workflow:
    stmt = select(Workflow).where(
        Workflow.tenant_id == tenant_id, Workflow.workflow_id == workflow_id, Workflow.is_current.is_(True)
    )
    obj = (await db.execute(stmt)).scalar_one_or_none()
    if obj is None:
        raise HTTPException(status_code=404, detail="Workflow not found")
    return obj


@router.get("", response_model=list[WorkflowRead])
async def list_workflows(
    skip: int = 0,
    limit: int = 100,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> list[Workflow]:
    stmt = (
        select(Workflow)
        .where(Workflow.tenant_id == ctx.tenant_id, Workflow.is_current.is_(True))
        .order_by(Workflow.name)
        .offset(skip)
        .limit(limit)
    )
    return list((await db.execute(stmt)).scalars().all())


@router.get("/{workflow_id}", response_model=WorkflowRead)
async def get_workflow(
    workflow_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> Workflow:
    return await _get_current(db, ctx.tenant_id, workflow_id)


@router.get("/{workflow_id}/versions", response_model=list[WorkflowRead])
async def list_workflow_versions(
    workflow_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> list[Workflow]:
    stmt = (
        select(Workflow)
        .where(Workflow.tenant_id == ctx.tenant_id, Workflow.workflow_id == workflow_id)
        .order_by(Workflow.version)
    )
    versions = list((await db.execute(stmt)).scalars().all())
    if not versions:
        raise HTTPException(status_code=404, detail="Workflow not found")
    return versions


@router.get("/{workflow_id}/versions/{version}", response_model=WorkflowRead)
async def get_workflow_version(
    workflow_id: uuid.UUID,
    version: int,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> Workflow:
    stmt = select(Workflow).where(
        Workflow.tenant_id == ctx.tenant_id, Workflow.workflow_id == workflow_id, Workflow.version == version
    )
    obj = (await db.execute(stmt)).scalar_one_or_none()
    if obj is None:
        raise HTTPException(status_code=404, detail="Workflow version not found")
    return obj


@router.post("", response_model=WorkflowRead, status_code=status.HTTP_201_CREATED)
async def create_workflow(
    payload: WorkflowCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> Workflow:
    obj = Workflow(
        tenant_id=ctx.tenant_id,
        name=payload.name,
        step_definitions=[s.model_dump(mode="json") for s in payload.step_definitions],
        settings=payload.settings.model_dump(mode="json"),
        status="draft",
        is_current=True,
        created_by=ctx.user_id,
    )
    db.add(obj)
    await db.flush()
    await db.refresh(obj)
    return obj


@router.patch("/{workflow_id}", response_model=WorkflowRead)
async def update_workflow_draft(
    workflow_id: uuid.UUID,
    payload: WorkflowUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Workflow:
    """
    In-place edits are only allowed on an unpublished draft. Once published,
    FR-WFD-006 requires structural edits to go through /new-version instead,
    so in-flight instances keep running against the version they started on.
    """
    obj = await _get_current(db, ctx.tenant_id, workflow_id)
    if obj.status != "draft":
        raise HTTPException(
            status_code=409,
            detail="This workflow is published; edit it via POST /workflows/{id}/new-version (FR-WFD-006).",
        )
    if payload.name is not None:
        obj.name = payload.name
    if payload.step_definitions is not None:
        obj.step_definitions = [s.model_dump(mode="json") for s in payload.step_definitions]
    if payload.settings is not None:
        obj.settings = payload.settings.model_dump(mode="json")
    await db.flush()
    await db.refresh(obj)
    return obj


@router.post("/{workflow_id}/publish", response_model=WorkflowRead)
async def publish_workflow(
    workflow_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "publish")),
    __: AuthContext = Depends(deny_vendor()),
) -> Workflow:
    obj = await _get_current(db, ctx.tenant_id, workflow_id)
    if obj.status == "published":
        return obj
    # Nothing that couldn't run goes live (app/crud/workflow_engine.py).
    problems = await workflow_engine.definition_problems(db, ctx.tenant_id, obj)
    if problems:
        raise HTTPException(status_code=422, detail="Fix these before publishing: " + " · ".join(problems))
    obj.status = "published"
    await db.flush()
    await db.refresh(obj)
    return obj


@router.post("/{workflow_id}/new-version", response_model=WorkflowRead, status_code=status.HTTP_201_CREATED)
async def new_workflow_version(
    workflow_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Workflow:
    """Draft a new version seeded from the current one. The old version stays
    published and unmodified — instances already running against it are
    unaffected (FR-WFD-006) — while the new draft becomes `is_current` for
    anyone opening the designer or starting a new instance next."""
    current = await _get_current(db, ctx.tenant_id, workflow_id)
    current.is_current = False
    await db.flush()

    new_version = Workflow(
        tenant_id=ctx.tenant_id,
        workflow_id=workflow_id,
        version=current.version + 1,
        name=current.name,
        step_definitions=current.step_definitions,
        settings=dict(current.settings or {}),
        status="draft",
        is_current=True,
        created_by=ctx.user_id,
    )
    db.add(new_version)
    await db.flush()
    await db.refresh(new_version)
    return new_version


@router.post("/{workflow_id}/duplicate", response_model=WorkflowRead, status_code=status.HTTP_201_CREATED)
async def duplicate_workflow(
    workflow_id: uuid.UUID,
    payload: WorkflowDuplicate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> Workflow:
    """A brand-new workflow (its own workflow_id, v1, draft) seeded from the
    source's current version: its steps plus the forms linked to them.
    Unlike /new-version the source is untouched and nothing is shared, so
    its instances and version history stay with it."""
    source = await _get_current(db, ctx.tenant_id, workflow_id)
    name = (payload.name or "").strip() or f"{source.name} (copy)"
    copy_ = Workflow(
        tenant_id=ctx.tenant_id,
        name=name[:200],
        step_definitions=copy.deepcopy(source.step_definitions or []),
        settings=dict(source.settings or {}),
        status="draft",
        is_current=True,
        created_by=ctx.user_id,
    )
    db.add(copy_)
    await db.flush()
    await db.refresh(copy_)

    links = (
        await db.execute(
            select(WorkflowStepForm).where(
                WorkflowStepForm.workflow_id == source.workflow_id, WorkflowStepForm.workflow_version == source.version
            )
        )
    ).scalars().all()
    for link in links:
        db.add(
            WorkflowStepForm(
                workflow_id=copy_.workflow_id,
                workflow_version=copy_.version,
                step_key=link.step_key,
                form_id=link.form_id,
                form_version=link.form_version,
            )
        )
    await db.flush()
    return copy_


@router.delete("/{workflow_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_workflow(
    workflow_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "delete")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    """Only removable while it has never been published — once published it
    may be referenced by workflow_instances, and FR-WFD-006/NFR-AUD-001 mean
    that history must not disappear."""
    stmt = select(Workflow).where(Workflow.tenant_id == ctx.tenant_id, Workflow.workflow_id == workflow_id)
    versions = list((await db.execute(stmt)).scalars().all())
    if not versions:
        raise HTTPException(status_code=404, detail="Workflow not found")
    if any(v.status == "published" for v in versions):
        raise HTTPException(
            status_code=409, detail="A published workflow cannot be deleted; its history must be preserved."
        )
    await db.execute(delete(Workflow).where(Workflow.tenant_id == ctx.tenant_id, Workflow.workflow_id == workflow_id))
    await db.flush()


# ---- Step-to-form links (FR-WFD-005) -------------------------------------------------------------

async def _get_version(db: AsyncSession, tenant_id: uuid.UUID, workflow_id: uuid.UUID, version: int) -> Workflow:
    stmt = select(Workflow).where(
        Workflow.tenant_id == tenant_id, Workflow.workflow_id == workflow_id, Workflow.version == version
    )
    obj = (await db.execute(stmt)).scalar_one_or_none()
    if obj is None:
        raise HTTPException(status_code=404, detail="Workflow version not found")
    return obj


@router.get("/{workflow_id}/versions/{version}/steps", response_model=list[WorkflowStepFormRead])
async def list_step_forms(
    workflow_id: uuid.UUID,
    version: int,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> list[WorkflowStepForm]:
    await _get_version(db, ctx.tenant_id, workflow_id, version)
    stmt = select(WorkflowStepForm).where(
        WorkflowStepForm.workflow_id == workflow_id, WorkflowStepForm.workflow_version == version
    )
    return list((await db.execute(stmt)).scalars().all())


@router.put("/{workflow_id}/versions/{version}/steps/{step_key}", response_model=WorkflowStepFormRead)
async def set_step_form(
    workflow_id: uuid.UUID,
    version: int,
    step_key: str,
    payload: WorkflowStepFormCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> WorkflowStepForm:
    workflow = await _get_version(db, ctx.tenant_id, workflow_id, version)
    if workflow.status != "draft":
        raise HTTPException(
            status_code=409,
            detail="This workflow version is published; link forms on a draft (via /new-version) instead.",
        )
    if not any(s.get("key") == step_key for s in workflow.step_definitions):
        raise HTTPException(status_code=404, detail=f"No step '{step_key}' in this workflow version's step_definitions")

    form = (
        await db.execute(
            select(Form).where(
                Form.tenant_id == ctx.tenant_id,
                Form.form_id == payload.form_id,
                Form.version == payload.form_version,
            )
        )
    ).scalar_one_or_none()
    if form is None:
        raise HTTPException(status_code=404, detail="Form version not found")

    link = (
        await db.execute(
            select(WorkflowStepForm).where(
                WorkflowStepForm.workflow_id == workflow_id,
                WorkflowStepForm.workflow_version == version,
                WorkflowStepForm.step_key == step_key,
            )
        )
    ).scalar_one_or_none()
    if link is None:
        link = WorkflowStepForm(
            workflow_id=workflow_id, workflow_version=version, step_key=step_key,
            form_id=payload.form_id, form_version=payload.form_version,
        )
        db.add(link)
    else:
        link.form_id = payload.form_id
        link.form_version = payload.form_version
    await db.flush()
    return link


@router.delete("/{workflow_id}/versions/{version}/steps/{step_key}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_step_form(
    workflow_id: uuid.UUID,
    version: int,
    step_key: str,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    await _get_version(db, ctx.tenant_id, workflow_id, version)
    link = (
        await db.execute(
            select(WorkflowStepForm).where(
                WorkflowStepForm.workflow_id == workflow_id,
                WorkflowStepForm.workflow_version == version,
                WorkflowStepForm.step_key == step_key,
            )
        )
    ).scalar_one_or_none()
    if link is None:
        raise HTTPException(status_code=404, detail="No form linked to this step")
    await db.delete(link)
    await db.flush()
