"""Helper script for persistence_lifecycle.robot E2E tests.

Provides subcommands exercising plan/action persistence through full
lifecycle transitions, restart simulation, argument/invariant ordering,
project link persistence, and concurrent session safeguards.

Usage:
    python robot/helper_persistence_lifecycle.py <subcommand>

Subcommands:
    full-lifecycle           Create action + plan, transition all phases
    restart-simulation       Write data, close DB, reopen, verify state
    reopen-plan-status       Close/reopen and check plan status survives
    concurrent-access        Two sessions against same DB file
    arguments-ordering       Verify ActionArgument ordering roundtrip
    invariants-ordering      Verify invariant list ordering roundtrip
    project-links            Verify ProjectLink persistence roundtrip
"""

from __future__ import annotations

import sys
import tempfile
from datetime import UTC, datetime
from pathlib import Path

# Ensure src is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
sys.path.insert(0, _SRC)

from sqlalchemy import create_engine  # noqa: E402
from sqlalchemy.orm import Session  # noqa: E402

from cleveragents.domain.models.core.action import (  # noqa: E402
    Action,
    ActionArgument,
    ActionState,
    ArgumentRequirement,
    ArgumentType,
)
from cleveragents.domain.models.core.plan import (  # noqa: E402
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.database.models import init_database  # noqa: E402
from cleveragents.infrastructure.database.repositories import (  # noqa: E402
    ActionRepository,
    LifecyclePlanRepository,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _temp_db() -> tuple[str, Session]:
    """Create a file-backed temp SQLite DB and return (path, session)."""
    tmp: str = tempfile.mktemp(suffix=".db")
    db_url: str = f"sqlite:///{tmp}"
    engine = init_database(db_url)
    session = Session(bind=engine)
    return tmp, session


def _make_action(name: str = "local/lifecycle-action") -> Action:
    """Create a minimal Action for FK satisfaction."""
    now: datetime = datetime(2026, 2, 1, tzinfo=UTC)
    return Action(
        namespaced_name=NamespacedName.parse(name),
        description="Lifecycle test action",
        definition_of_done="All phases pass",
        strategy_actor="local/strat",
        execution_actor="local/exec",
        state=ActionState.AVAILABLE,
        created_at=now,
        updated_at=now,
    )


def _make_plan(
    plan_id: str,
    action_name: str = "local/lifecycle-action",
    phase: PlanPhase = PlanPhase.ACTION,
    state: ProcessingState = ProcessingState.QUEUED,
    parent_plan_id: str | None = None,
    root_plan_id: str | None = None,
    project_links: list[ProjectLink] | None = None,
) -> Plan:
    """Create a Plan domain object with sensible defaults."""
    now: datetime = datetime(2026, 3, 1, tzinfo=UTC)
    return Plan(
        identity=PlanIdentity(
            plan_id=plan_id,
            parent_plan_id=parent_plan_id,
            root_plan_id=root_plan_id,
            attempt=1,
        ),
        namespaced_name=NamespacedName(namespace="local", name="lifecycle-plan"),
        action_name=action_name,
        description="Lifecycle test plan",
        definition_of_done="E2E assertions pass",
        phase=phase,
        processing_state=state,
        strategy_actor="local/strat",
        execution_actor="local/exec",
        project_links=project_links or [ProjectLink(project_name="local/proj-lc")],
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
        created_by="robot-lifecycle",
        tags=["lifecycle"],
        reusable=True,
        read_only=False,
    )


# ---------------------------------------------------------------------------
# Subcommand: full-lifecycle
# ---------------------------------------------------------------------------


def _full_lifecycle() -> None:
    """Create plan, walk through every phase to terminal applied state."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        action_repo = ActionRepository(session_factory=factory)
        action_repo.create(_make_action())
        session.commit()

        plan_repo = LifecyclePlanRepository(session_factory=factory)
        plan = _make_plan("01HV00000000000000MCFM0001")
        plan_repo.create(plan)
        session.commit()

        # Verify initial state
        got = plan_repo.get("01HV00000000000000MCFM0001")
        assert got is not None, "plan not found after create"
        assert got.phase == PlanPhase.ACTION

        # action -> strategize
        got.phase = PlanPhase.STRATEGIZE
        got.processing_state = ProcessingState.PROCESSING
        got.timestamps.updated_at = datetime.now(tz=UTC)
        plan_repo.update(got)
        session.commit()

        got = plan_repo.get("01HV00000000000000MCFM0001")
        assert got is not None
        assert got.phase == PlanPhase.STRATEGIZE

        # strategize -> execute
        got.phase = PlanPhase.EXECUTE
        got.processing_state = ProcessingState.QUEUED
        got.timestamps.updated_at = datetime.now(tz=UTC)
        plan_repo.update(got)
        session.commit()

        got = plan_repo.get("01HV00000000000000MCFM0001")
        assert got is not None
        assert got.phase == PlanPhase.EXECUTE

        # execute -> apply (terminal)
        got.phase = PlanPhase.APPLY
        got.processing_state = ProcessingState.APPLIED
        got.timestamps.updated_at = datetime.now(tz=UTC)
        plan_repo.update(got)
        session.commit()

        got = plan_repo.get("01HV00000000000000MCFM0001")
        assert got is not None
        assert got.phase == PlanPhase.APPLY
        assert got.processing_state == ProcessingState.APPLIED
        assert got.is_terminal

        print("full-lifecycle-ok")
    finally:
        session.close()
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: restart-simulation
# ---------------------------------------------------------------------------


def _restart_simulation() -> None:
    """Write plan, close DB entirely, reopen from disk and verify."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        action_repo = ActionRepository(session_factory=factory)
        action_repo.create(_make_action())
        session.commit()

        plan_repo = LifecyclePlanRepository(session_factory=factory)
        plan = _make_plan(
            "01HV00000000000000MCRS0001",
            phase=PlanPhase.EXECUTE,
            state=ProcessingState.PROCESSING,
        )
        plan_repo.create(plan)
        session.commit()

        # Simulate process restart
        session.close()

        # Reopen from disk
        engine2 = create_engine(f"sqlite:///{tmp}", echo=False)
        session2 = Session(bind=engine2)
        factory2 = lambda: session2  # noqa: E731
        plan_repo2 = LifecyclePlanRepository(session_factory=factory2)

        retrieved = plan_repo2.get("01HV00000000000000MCRS0001")
        assert retrieved is not None, "plan missing after restart"
        assert retrieved.phase == PlanPhase.EXECUTE
        assert retrieved.processing_state == ProcessingState.PROCESSING
        assert retrieved.description == "Lifecycle test plan"
        assert len(retrieved.project_links) == 1
        assert retrieved.project_links[0].project_name == "local/proj-lc"

        session2.close()
        engine2.dispose()
        print("restart-simulation-ok")
    finally:
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: reopen-plan-status
# ---------------------------------------------------------------------------


def _reopen_plan_status() -> None:
    """Close + reopen DB and verify plan phase/state unchanged."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        action_repo = ActionRepository(session_factory=factory)
        action_repo.create(_make_action())
        session.commit()

        plan_repo = LifecyclePlanRepository(session_factory=factory)
        plan = _make_plan(
            "01HV00000000000000MCRQ0001",
            phase=PlanPhase.STRATEGIZE,
            state=ProcessingState.PROCESSING,
        )
        plan_repo.create(plan)
        session.commit()
        session.close()

        # Reopen
        engine2 = create_engine(f"sqlite:///{tmp}", echo=False)
        session2 = Session(bind=engine2)
        factory2 = lambda: session2  # noqa: E731
        plan_repo2 = LifecyclePlanRepository(session_factory=factory2)

        got = plan_repo2.get("01HV00000000000000MCRQ0001")
        assert got is not None, "plan missing after reopen"
        assert got.phase == PlanPhase.STRATEGIZE, f"bad phase: {got.phase}"
        assert got.processing_state == ProcessingState.PROCESSING
        assert got.tags == ["lifecycle"]

        session2.close()
        engine2.dispose()
        print("reopen-plan-status-ok")
    finally:
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: concurrent-access
# ---------------------------------------------------------------------------


def _concurrent_access() -> None:
    """Two independent sessions see each other's committed data."""
    tmp, session1 = _temp_db()
    factory1 = lambda: session1  # noqa: E731
    try:
        action_repo = ActionRepository(session_factory=factory1)
        action_repo.create(_make_action())
        session1.commit()

        plan_repo1 = LifecyclePlanRepository(session_factory=factory1)
        plan = _make_plan("01HV00000000000000MCCC0001")
        plan_repo1.create(plan)
        session1.commit()

        # Second session on same DB
        engine2 = create_engine(f"sqlite:///{tmp}", echo=False)
        session2 = Session(bind=engine2)
        factory2 = lambda: session2  # noqa: E731
        plan_repo2 = LifecyclePlanRepository(session_factory=factory2)

        got = plan_repo2.get("01HV00000000000000MCCC0001")
        assert got is not None, "concurrent session cannot see committed plan"
        assert got.description == "Lifecycle test plan"

        session2.close()
        engine2.dispose()
        print("concurrent-access-ok")
    finally:
        session1.close()
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: arguments-ordering
# ---------------------------------------------------------------------------


def _arguments_ordering() -> None:
    """Verify ActionArgument ordering survives persistence roundtrip."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        arg_names: list[str] = ["alpha", "beta", "gamma", "delta"]
        action = Action(
            namespaced_name=NamespacedName.parse("local/ordered-args-action"),
            description="Action with ordered arguments",
            definition_of_done="Args order preserved",
            strategy_actor="local/strat",
            execution_actor="local/exec",
            arguments=[
                ActionArgument(
                    name=n,
                    arg_type=ArgumentType.STRING,
                    requirement=ArgumentRequirement.REQUIRED,
                    description=f"Arg {n}",
                    position=i,
                )
                for i, n in enumerate(arg_names)
            ],
            state=ActionState.AVAILABLE,
            created_at=datetime(2026, 2, 1, tzinfo=UTC),
            updated_at=datetime(2026, 2, 1, tzinfo=UTC),
        )
        repo = ActionRepository(session_factory=factory)
        repo.create(action)
        session.commit()

        fetched = repo.get_by_name("local/ordered-args-action")
        assert fetched is not None, "action not found"
        retrieved_names: list[str] = [a.name for a in fetched.arguments]
        assert retrieved_names == arg_names, (
            f"ordering mismatch: {retrieved_names} != {arg_names}"
        )
        print("arguments-ordering-ok")
    finally:
        session.close()
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: invariants-ordering
# ---------------------------------------------------------------------------


def _invariants_ordering() -> None:
    """Verify invariant list ordering survives persistence roundtrip."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        invariants: list[str] = [
            "No breaking changes",
            "All tests pass",
            "Coverage above 97%",
        ]
        action = Action(
            namespaced_name=NamespacedName.parse("local/ordered-inv-action"),
            description="Action with ordered invariants",
            definition_of_done="Invariants order preserved",
            strategy_actor="local/strat",
            execution_actor="local/exec",
            invariants=invariants,
            state=ActionState.AVAILABLE,
            created_at=datetime(2026, 2, 1, tzinfo=UTC),
            updated_at=datetime(2026, 2, 1, tzinfo=UTC),
        )
        repo = ActionRepository(session_factory=factory)
        repo.create(action)
        session.commit()

        fetched = repo.get_by_name("local/ordered-inv-action")
        assert fetched is not None, "action not found"
        assert fetched.invariants == invariants, (
            f"ordering mismatch: {fetched.invariants} != {invariants}"
        )
        print("invariants-ordering-ok")
    finally:
        session.close()
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Subcommand: project-links
# ---------------------------------------------------------------------------


def _project_links() -> None:
    """Verify ProjectLink persistence through create/restart cycle."""
    tmp, session = _temp_db()
    factory = lambda: session  # noqa: E731
    try:
        action_repo = ActionRepository(session_factory=factory)
        action_repo.create(_make_action())
        session.commit()

        links: list[ProjectLink] = [
            ProjectLink(project_name="local/proj-alpha"),
            ProjectLink(project_name="local/proj-beta"),
        ]
        plan_repo = LifecyclePlanRepository(session_factory=factory)
        plan = _make_plan(
            "01HV00000000000000MCPM0001",
            project_links=links,
        )
        plan_repo.create(plan)
        session.commit()

        # Close and reopen (restart simulation)
        session.close()

        engine2 = create_engine(f"sqlite:///{tmp}", echo=False)
        session2 = Session(bind=engine2)
        factory2 = lambda: session2  # noqa: E731
        plan_repo2 = LifecyclePlanRepository(session_factory=factory2)

        got = plan_repo2.get("01HV00000000000000MCPM0001")
        assert got is not None, "plan with project links missing"
        retrieved_names: list[str] = [pl.project_name for pl in got.project_links]
        assert "local/proj-alpha" in retrieved_names, "proj-alpha missing"
        assert "local/proj-beta" in retrieved_names, "proj-beta missing"
        assert len(got.project_links) == 2, (
            f"expected 2 links, got {len(got.project_links)}"
        )

        session2.close()
        engine2.dispose()
        print("project-links-ok")
    finally:
        Path(tmp).unlink(missing_ok=True)


# ---------------------------------------------------------------------------
# Main dispatcher
# ---------------------------------------------------------------------------

_COMMANDS: dict[str, object] = {
    "full-lifecycle": _full_lifecycle,
    "restart-simulation": _restart_simulation,
    "reopen-plan-status": _reopen_plan_status,
    "concurrent-access": _concurrent_access,
    "arguments-ordering": _arguments_ordering,
    "invariants-ordering": _invariants_ordering,
    "project-links": _project_links,
}


def main() -> None:
    """Dispatch to the requested subcommand."""
    if len(sys.argv) < 2:
        raise SystemExit(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
    command: str = sys.argv[1]
    handler = _COMMANDS.get(command)
    if handler is None:
        raise SystemExit(f"Unknown command: {command}")
    handler()  # type: ignore[operator]


if __name__ == "__main__":
    main()
