"""Run 及其行为记录。

runs / run_steps / recovery_actions 是**读模型**（投影）。
唯一真相是 run_events（append-only）—— 见 ADR-0002。

仓储层刻意不提供 update_run_state()：改状态的唯一入口是 events/recorder.py。
"""
from __future__ import annotations

import datetime as _dt
from typing import Any, Dict, List, Optional

from sqlalchemy import (
    Boolean,
    DateTime,
    Float,
    ForeignKey,
    Index,
    Integer,
    JSON,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column

from ..enums import EventSource, RunState, Severity, StepState
from .base import Base, TimestampMixin, pk


class Run(Base, TimestampMixin):
    __tablename__ = "runs"

    id: Mapped[str] = pk("run")
    name: Mapped[Optional[str]] = mapped_column(String(255))

    workflow_id: Mapped[str] = mapped_column(ForeignKey("workflows.id"), nullable=False)
    workflow_key: Mapped[str] = mapped_column(String(64), nullable=False)
    workflow_version: Mapped[int] = mapped_column(Integer, nullable=False)
    spec_hash: Mapped[str] = mapped_column(String(64), nullable=False)

    state: Mapped[str] = mapped_column(String(16), default=RunState.QUEUED.value, nullable=False)
    verdict: Mapped[Optional[str]] = mapped_column(String(16))

    dut_id: Mapped[Optional[str]] = mapped_column(ForeignKey("duts.id"))
    test_host_id: Mapped[Optional[str]] = mapped_column(ForeignKey("test_hosts.id"))
    firmware_a_id: Mapped[Optional[str]] = mapped_column(ForeignKey("firmware_artifacts.id"))
    firmware_b_id: Mapped[Optional[str]] = mapped_column(ForeignKey("firmware_artifacts.id"))

    params: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
    # 任务开始时冻结的不可变快照。固件之外任何一项变了，A/B 对比结论就不成立。
    env_fingerprint: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
    env_fingerprint_hash: Mapped[Optional[str]] = mapped_column(String(64))

    loop_target: Mapped[int] = mapped_column(Integer, default=1)
    loop_done: Mapped[int] = mapped_column(Integer, default=0)

    # 最近一次检查点：{"loop_index": 37, "next_step_key": "...", "dut_fw": "A"}
    checkpoint: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)

    started_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
    ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)

    # ROI 凭据（方案 §9.3）：这两个字段是销售武器，从第一行代码就记。
    unattended_completion: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    human_touches: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    recovery_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)

    # 事件序号分配器（单点递增，保证时间线可重放，见 ADR-0002）
    event_seq: Mapped[int] = mapped_column(Integer, default=0, nullable=False)

    created_by: Mapped[Optional[str]] = mapped_column(String(64))
    freeze_reason: Mapped[Optional[str]] = mapped_column(Text)


class RunStep(Base, TimestampMixin):
    """物化后的步骤实例。循环体在物化时按 loop_index 展开。"""

    __tablename__ = "run_steps"
    __table_args__ = (
        UniqueConstraint("run_id", "seq", name="uq_step_run_seq"),
        Index("ix_step_run_state", "run_id", "state"),
    )

    id: Mapped[str] = pk("step")
    run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
    seq: Mapped[int] = mapped_column(Integer, nullable=False)
    loop_index: Mapped[Optional[int]] = mapped_column(Integer)

    step_key: Mapped[str] = mapped_column(String(64), nullable=False)
    step_type: Mapped[str] = mapped_column(String(32), nullable=False)
    adapter: Mapped[str] = mapped_column(String(32), nullable=False)
    template: Mapped[Optional[str]] = mapped_column(String(64))
    params: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)

    state: Mapped[str] = mapped_column(String(16), default=StepState.PENDING.value, nullable=False)
    attempt: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    max_retry: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    timeout_s: Mapped[float] = mapped_column(Float, default=600.0, nullable=False)

    # 非幂等步骤（默认）中断后不自动重跑 —— "自动重试把盘刷坏"是最容易砸招牌的失败模式
    idempotent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    danger: Mapped[str] = mapped_column(String(16), default="none")
    on_fail: Mapped[str] = mapped_column(String(16), default="retry")
    checkpoint_after: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

    # 租约：Agent 领了活就死了 → 租约到期 → 步骤回 PENDING 重新派发
    leased_by: Mapped[Optional[str]] = mapped_column(String(32))
    lease_expires_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)

    started_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
    ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
    exit_code: Mapped[Optional[int]] = mapped_column(Integer)
    error_class: Mapped[Optional[str]] = mapped_column(String(32))
    result: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)   # UnifiedResult
    log_uri: Mapped[Optional[str]] = mapped_column(String(512))


class RunEvent(Base):
    """Append-only 事件流 —— 唯一真相。

    seq 由控制平面单点分配（不按时间戳排序）：主机断电、时钟漂移、
    带外与主机时钟不同步都会让时间戳乱序。Agent 事件与带外事件进同一条序列，
    双通道观测的交叉校验靠它。
    """

    __tablename__ = "run_events"
    __table_args__ = (
        UniqueConstraint("run_id", "seq", name="uq_event_run_seq"),
        # 网络抖动重发不产生重复事件（见 agent-protocol.md）
        UniqueConstraint("run_id", "idempotency_key", name="uq_event_idem"),
        Index("ix_event_run_seq", "run_id", "seq"),
    )

    id: Mapped[str] = pk("ev")
    run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
    seq: Mapped[int] = mapped_column(Integer, nullable=False)
    ts: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)

    source: Mapped[str] = mapped_column(String(16), default=EventSource.CONTROL_PLANE.value)
    kind: Mapped[str] = mapped_column(String(48), nullable=False)
    severity: Mapped[str] = mapped_column(String(16), default=Severity.INFO.value)

    step_id: Mapped[Optional[str]] = mapped_column(String(32))
    loop_index: Mapped[Optional[int]] = mapped_column(Integer)
    message: Mapped[Optional[str]] = mapped_column(Text)
    payload: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)

    idempotency_key: Mapped[Optional[str]] = mapped_column(String(64))


class RecoveryAction(Base):
    """每一级恢复的留证记录。恢复前抓画面/温度，恢复后验证 DUT 重新枚举。"""

    __tablename__ = "recovery_actions"

    id: Mapped[str] = pk("rec")
    run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
    step_id: Mapped[Optional[str]] = mapped_column(String(32))
    loop_index: Mapped[Optional[int]] = mapped_column(Integer)

    level: Mapped[str] = mapped_column(String(24), nullable=False)
    trigger: Mapped[str] = mapped_column(String(32), nullable=False)
    outcome: Mapped[Optional[str]] = mapped_column(String(16))

    started_at: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
    ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
    detail: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
    evidence_uris: Mapped[List[Any]] = mapped_column(JSON, default=list)


class ResourceLock(Base):
    """DUT / 主机独占锁。PREFLIGHT 获取，终态释放。

    唯一约束在 (resource_type, resource_id) —— 数据库来保证互斥，不靠应用层自觉。
    """

    __tablename__ = "resource_locks"
    __table_args__ = (
        UniqueConstraint("resource_type", "resource_id", name="uq_lock_resource"),
    )

    id: Mapped[str] = pk("lock")
    resource_type: Mapped[str] = mapped_column(String(16), nullable=False)   # dut / test_host / oob
    resource_id: Mapped[str] = mapped_column(String(32), nullable=False)
    run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
    acquired_at: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
    expires_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
