"""Materialize a published Workflow snapshot into sequential RunStep rows.

The planner deliberately supports a small, deterministic subset of the workflow
schema.  It never evaluates expressions and it never turns free-form text into a
command.  Host Agents only receive already-published adapter/template references.
"""
from __future__ import annotations

import re
from typing import Any, Dict, List, Optional

from sqlalchemy.ext.asyncio import AsyncSession

from ..config import get_settings
from ..enums import OnFail, StepType
from ..models import Run, RunStep, Workflow

_PARAM = re.compile(r"^\{\{\s*params\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$")
_LOOP_INDEX = re.compile(r"^\{\{\s*loop\.index\s*\}\}$")
_HOST_STEP_TYPES = {
    StepType.COMMAND.value,
    StepType.DEVICE_CHECK.value,
    StepType.WAIT.value,
}


class WorkflowPlanningError(ValueError):
    """The published workflow cannot be safely materialized."""


def _resolve(value: Any, run_params: Dict[str, Any], loop_index: Optional[int]) -> Any:
    """Resolve only exact, allow-listed template tokens; no expression evaluation."""

    if isinstance(value, str):
        parameter = _PARAM.fullmatch(value)
        if parameter:
            key = parameter.group(1)
            if key not in run_params:
                raise WorkflowPlanningError("工作流参数不存在: {0}".format(key))
            return run_params[key]
        if _LOOP_INDEX.fullmatch(value):
            if loop_index is None:
                raise WorkflowPlanningError("loop.index 只能在循环体内使用")
            return loop_index
        if "{{" in value or "}}" in value:
            raise WorkflowPlanningError("模板只允许完整的 params.X 或 loop.index 占位符")
        return value
    if isinstance(value, dict):
        return {key: _resolve(item, run_params, loop_index) for key, item in value.items()}
    if isinstance(value, list):
        return [_resolve(item, run_params, loop_index) for item in value]
    return value


def _step_row(
    *,
    run: Run,
    spec: Dict[str, Any],
    seq: int,
    loop_index: Optional[int],
    checkpoint_after: bool,
) -> RunStep:
    step_type = str(spec.get("type", ""))
    if step_type not in _HOST_STEP_TYPES:
        raise WorkflowPlanningError("Host Agent 不支持步骤类型: {0}".format(step_type or "<empty>"))
    key = str(spec.get("key", "")).strip()
    adapter = str(spec.get("adapter", "")).strip()
    if not key or not adapter:
        raise WorkflowPlanningError("步骤 key 与 adapter 不能为空")

    retry = int(spec.get("retry", 0))
    timeout_s = float(spec.get("timeout_s", get_settings().timing.step_default_timeout_s))
    if retry < 0 or retry > 20:
        raise WorkflowPlanningError("步骤 retry 必须在 0..20 之间")
    if timeout_s <= 0:
        raise WorkflowPlanningError("步骤 timeout_s 必须大于 0")

    return RunStep(
        run_id=run.id,
        seq=seq,
        loop_index=loop_index,
        step_key=key,
        step_type=step_type,
        adapter=adapter,
        template=spec.get("template"),
        params=_resolve(dict(spec.get("params") or {}), run.params, loop_index),
        max_retry=retry,
        timeout_s=timeout_s,
        idempotent=bool(spec.get("idempotent", False)),
        danger=str(spec.get("danger", "none")),
        on_fail=str(spec.get("on_fail", OnFail.RETRY.value)),
        checkpoint_after=checkpoint_after,
    )


def plan_run_steps(run: Run, workflow: Workflow) -> List[RunStep]:
    """Expand one workflow snapshot into the exact ordered steps for a Run."""

    if not workflow.published:
        raise WorkflowPlanningError("工作流尚未发布")
    raw_steps = workflow.spec.get("steps") if isinstance(workflow.spec, dict) else None
    if not isinstance(raw_steps, list) or not raw_steps:
        raise WorkflowPlanningError("工作流没有可执行步骤")

    planned: List[RunStep] = []
    seq = 1
    for raw in raw_steps:
        if not isinstance(raw, dict):
            raise WorkflowPlanningError("工作流步骤必须是 object")
        step_type = str(raw.get("type", ""))

        # Safety preflight and evidence/report generation live in the control plane.
        if step_type in {StepType.PRECHECK.value, StepType.REPORT.value}:
            continue

        if step_type == StepType.LOOP.value:
            body = raw.get("body")
            if not isinstance(body, list) or not body:
                raise WorkflowPlanningError("循环步骤必须包含非空 body")
            for loop_index in range(1, run.loop_target + 1):
                for index, child in enumerate(body):
                    if not isinstance(child, dict):
                        raise WorkflowPlanningError("循环体步骤必须是 object")
                    planned.append(
                        _step_row(
                            run=run,
                            spec=child,
                            seq=seq,
                            loop_index=loop_index,
                            checkpoint_after=bool(child.get("checkpoint", False))
                            or (bool(raw.get("checkpoint", False)) and index == len(body) - 1),
                        )
                    )
                    seq += 1
            continue

        planned.append(
            _step_row(
                run=run,
                spec=raw,
                seq=seq,
                loop_index=None,
                checkpoint_after=bool(raw.get("checkpoint", False)),
            )
        )
        seq += 1

    if not planned:
        raise WorkflowPlanningError("工作流没有 Host Agent 可执行步骤")
    return planned


async def materialize_run_steps(
    session: AsyncSession, run: Run, workflow: Workflow
) -> List[RunStep]:
    planned = plan_run_steps(run, workflow)
    session.add_all(planned)
    await session.flush()
    return planned
