"""Reusable scripted-agent arcs + seeding for the e2e smoke scenarios.

The company is seeded ONCE per stack session (canonical slugs — the A2A
permission model resolves roles/teams from the static ``agents_config``
registry, so slugs must match it). Projects and tasks are seeded per test
with unique slugs so scenarios never collide on constraints.
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any
from uuid import uuid4

from tests.e2e_smoke.harness import E2EStack, ScriptedAgent, expect_error, expect_ok

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession


class Company:
    """Seeded canonical agents (ids) — one per stack session."""

    dev_id: Any
    qa_id: Any
    doc_id: Any
    cell_pm_id: Any
    main_pm_id: Any
    pr_reviewer_id: Any
    ceo_id: Any
    hom_id: Any  # Head of Marketing — Board role


_COMPANY_CACHE: dict[str, Company] = {}


def seed_company(stack: E2EStack) -> Company:
    """Seed the canonical agents once; return their ids on every call."""
    if "company" in _COMPANY_CACHE:
        return _COMPANY_CACHE["company"]

    from roboco.db.tables import AgentTable
    from roboco.models import AgentRole, AgentStatus, Team

    out = Company()

    async def _run(session: AsyncSession) -> None:
        def agent(slug: str, role: AgentRole, team: Team | None) -> AgentTable:
            row = AgentTable(
                id=uuid4(),
                name=slug,
                slug=slug,
                role=role,
                team=team,
                status=AgentStatus.ACTIVE,
                model_config={},
                system_prompt=slug,
                capabilities=[],
                permissions={},
                metrics={},
            )
            session.add(row)
            return row

        dev = agent("be-dev-1", AgentRole.DEVELOPER, Team.BACKEND)
        qa = agent("be-qa", AgentRole.QA, Team.BACKEND)
        doc = agent("be-doc", AgentRole.DOCUMENTER, Team.BACKEND)
        cell_pm = agent("be-pm", AgentRole.CELL_PM, Team.BACKEND)
        main_pm = agent("main-pm", AgentRole.MAIN_PM, None)
        reviewer = agent("pr-reviewer-1", AgentRole.PR_REVIEWER, None)
        ceo = agent("ceo", AgentRole.CEO, None)
        hom = agent("head-marketing", AgentRole.HEAD_MARKETING, Team.BOARD)
        await session.flush()
        out.ceo_id = ceo.id
        out.dev_id = dev.id
        out.qa_id = qa.id
        out.doc_id = doc.id
        out.cell_pm_id = cell_pm.id
        out.main_pm_id = main_pm.id
        out.pr_reviewer_id = reviewer.id
        out.hom_id = hom.id

    stack.run_db(_run)
    _COMPANY_CACHE["company"] = out
    return out


def seed_project(stack: E2EStack, company: Company) -> tuple[Any, str]:
    """Seed a project rooted at the shared bare origin; unique slug per test."""
    from roboco.db.tables import ProjectTable
    from roboco.models import Team
    from roboco.utils.crypto import encrypt_token

    slug = f"e2e-proj-{uuid4().hex[:6]}"
    holder: dict[str, Any] = {}

    async def _run(session: AsyncSession) -> None:
        project = ProjectTable(
            id=uuid4(),
            name=f"E2E {slug}",
            slug=slug,
            git_url=str(stack.origin),
            default_branch="master",
            protected_branches=["master"],
            assigned_cell=Team.BACKEND,
            created_by=company.main_pm_id,
            is_active=True,
            git_token_encrypted=encrypt_token("e2e-dummy-token"),
        )
        session.add(project)
        await session.flush()
        holder["id"] = project.id

    stack.run_db(_run)
    return holder["id"], slug


def seed_task(stack: E2EStack, **overrides: Any) -> Any:
    """Seed one task row; caller passes the fields that matter."""
    from roboco.db.tables import TaskTable
    from roboco.models import Team
    from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType

    fields: dict[str, Any] = {
        "id": uuid4(),
        "acceptance_criteria": ["done"],
        "status": TaskStatus.PENDING,
        "priority": 2,
        "task_type": TaskType.CODE,
        "nature": TaskNature.TECHNICAL,
        "estimated_complexity": Complexity.LOW,
        "team": Team.BACKEND,
        "confirmed_by_human": True,
    }
    fields.update(overrides)

    async def _run(session: AsyncSession) -> None:
        session.add(TaskTable(**fields))

    stack.run_db(_run)
    return fields["id"]


def task_state(stack: E2EStack, task_id: Any) -> dict[str, Any]:
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> dict[str, Any]:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        return {
            "status": str(row.status),
            "branch_name": row.branch_name,
            "pr_number": row.pr_number,
            "docs_complete": row.docs_complete,
            "assigned_to": row.assigned_to,
        }

    state: dict[str, Any] = stack.run_db(_run)
    return state


def open_finding_ids(stack: E2EStack, task_id: Any) -> list[str]:
    """Open revision-findings ledger row ids for a task — the ids a scripted
    PM/dev must resolve before FINDINGS_ADDRESSED lets it resubmit."""
    from roboco.db.tables import TaskReviewFindingTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> list[str]:
        rows = (
            (
                await session.execute(
                    select(TaskReviewFindingTable.id).where(
                        TaskReviewFindingTable.task_id == task_id,
                        TaskReviewFindingTable.status == "open",
                    )
                )
            )
            .scalars()
            .all()
        )
        return [str(row) for row in rows]

    ids: list[str] = stack.run_db(_run)
    return ids


def wait_for_status(
    stack: E2EStack,
    task_id: Any,
    expected: str,
    *,
    timeout: float = 10.0,
    interval: float = 0.25,
) -> dict[str, Any]:
    """Poll ``task_state`` until ``status == expected`` or timeout.

    The e2e stack commits on the uvicorn thread's event loop and reads via a
    separate loop (``run_db`` -> ``asyncio.run`` with a fresh engine). A
    terminal single point-read can race a still-draining completion hook on a
    contended runner and observe a pre-terminal state; the bounded poll
    absorbs that transient. A genuine state bug still surfaces: the timeout
    branch asserts against the last-read state, so a real regression fails
    loudly with the actual (non-terminal) state instead of a misleading
    one-shot mismatch.
    """
    deadline = time.monotonic() + timeout
    last: dict[str, Any] = {}
    while True:
        last = task_state(stack, task_id)
        if last["status"] == expected:
            return last
        if time.monotonic() >= deadline:
            assert last["status"] == expected, last
        time.sleep(interval)


def dispatcher_assign(stack: E2EStack, task_id: Any, agent_id: Any) -> None:
    """Mirror the dispatcher's claim-for-PM lane (_dispatch_pm_review_work):
    pr_pass clears ownership by design and the orchestrator re-claims the
    task for the owning PM before spawning it."""
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> None:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        row.assigned_to = agent_id
        row.active_claimant_id = agent_id

    stack.run_db(_run)


def set_branch_name(stack: E2EStack, task_id: Any, branch_name: str) -> None:
    """Directly set a task's ``branch_name`` — a data field, not a lifecycle
    transition (mirrors ``dispatcher_assign``'s style: standing in for a
    system-side effect the real claim path would otherwise perform)."""
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> None:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        row.branch_name = branch_name

    stack.run_db(_run)


def wire_dependency(stack: E2EStack, dependent_id: Any, depends_on_id: Any) -> None:
    """Wire a real dependency edge the same way production sequencing does
    (``TaskService.add_dependency``) — not a direct status write."""
    from roboco.services.task import get_task_service

    async def _run(session: AsyncSession) -> None:
        await get_task_service(session).add_dependency(dependent_id, depends_on_id)

    stack.run_db(_run)


def origin_branch(stack: E2EStack, name: str, start: str = "master") -> None:
    """Create + push a branch in the shared origin via the admin clone."""
    from tests.e2e_smoke.harness import _git

    admin = stack.github.admin_clone
    _git(admin, "fetch", "origin", "--prune")
    _git(admin, "checkout", "-B", name, f"origin/{start}")
    _git(admin, "push", "origin", name)


def origin_commit(
    stack: E2EStack, branch: str, path: str, content: str, message: str
) -> None:
    """Land a commit on a branch in the origin via the admin clone —
    stands in for dev work advancing a branch between scripted turns."""
    from tests.e2e_smoke.harness import _git

    admin = stack.github.admin_clone
    _git(admin, "fetch", "origin", "--prune")
    _git(admin, "checkout", "-B", branch, f"origin/{branch}")
    (admin / path).write_text(content)
    _git(admin, "add", path)
    _git(admin, "commit", "-m", message)
    _git(admin, "push", "origin", branch)


def origin_file(stack: E2EStack, branch: str, path: str) -> str | None:
    """Read a file's content at a branch tip in the origin, or None."""
    import subprocess

    from tests.e2e_smoke.harness import _git

    try:
        return _git(stack.github.origin, "show", f"{branch}:{path}")
    except subprocess.CalledProcessError:
        return None


# ---------------------------------------------------------------------------
# Arcs — each drives one role through one lifecycle segment, gates and all
# ---------------------------------------------------------------------------


def dev_arc(
    stack: E2EStack,
    company: Company,
    project_slug: str,
    task_id: Any,
    *,
    work: tuple[str, str] = ("greeting.txt", "Hello from the e2e smoke agent!\n"),
) -> None:
    """PENDING (pre-assigned) → awaiting_qa: claim, work, commit, PR, submit."""
    filename, content = work
    tid = str(task_id)
    dev = ScriptedAgent(stack, company.dev_id, "be-dev-1", "developer")

    env = expect_ok(dev.flow("give_me_work"), "dev give_me_work")
    assert env.get("task_id") == tid, f"expected task {tid}, got: {env}"

    def _claim() -> dict[str, Any]:
        return dev.flow(
            "i_will_work_on",
            task_id=tid,
            plan=(
                f"Create {filename} at the repository root with the required "
                "content, commit it on the task branch with the task-prefixed "
                "message, push the branch to origin, open the pull request "
                "against the base branch, and self-verify every acceptance "
                "criterion by re-reading the committed file content."
            ),
            steps=[
                {
                    "title": f"Write {filename}",
                    "description": (
                        f"Create {filename} at the repo root containing the "
                        "required content for the acceptance criteria."
                    ),
                },
                {
                    "title": "Commit and push",
                    "description": (
                        "Commit the new file on the task branch with a "
                        "task-prefixed message and push it to origin."
                    ),
                },
                {
                    "title": "Open PR and self-verify",
                    "description": (
                        "Open the pull request against the base branch and "
                        "re-read the file to confirm the criteria hold."
                    ),
                },
            ],
            technical_considerations=["Plain text file; no build impact."],
            risks=[
                {
                    "risk": "None of substance — purely additive file.",
                    "mitigation": "Self-verify the file content before submit.",
                }
            ],
            open_questions=[],
        )

    # Real choreography: the composed claim succeeds and stays; the
    # post-claim tracing gate demands the claim-time note; the retry
    # short-circuits as re-entry.
    expect_error(_claim(), "tracing_gap", "dev first i_will_work_on")
    expect_ok(
        dev.do(
            "note",
            scope="note",
            task_id=tid,
            text=(
                "Initial assessment: a single additive text file at the repo "
                "root satisfies the acceptance criteria; no existing code is "
                "touched, so risk is minimal and the plan is a three-step "
                "write/commit/PR sequence."
            ),
        ),
        "dev note at claim",
    )
    expect_ok(_claim(), "dev i_will_work_on retry")

    workspace = stack.workspace_of(project_slug, "backend", "be-dev-1")
    workdir = workspace / ".worktrees" / tid[:8]
    assert workdir.is_dir(), f"per-task worktree missing at {workdir}"
    (workdir / filename).write_text(content)

    expect_ok(
        dev.do(
            "commit",
            message=f"feat: add {filename} with the required greeting content",
            files=[filename],
        ),
        "dev commit",
    )
    expect_ok(
        dev.do(
            "note",
            scope="note",
            task_id=tid,
            text=(
                f"{filename} written and committed on the task branch; "
                "opening the PR next, then self-verifying the acceptance "
                "criteria before submit."
            ),
        ),
        "dev progress note",
    )
    env = expect_ok(dev.flow("open_pr", task_id=tid), "dev open_pr")
    assert task_state(stack, task_id)["pr_number"], f"no PR recorded: {env}"

    criteria = _criteria_text(stack, task_id)
    expect_ok(
        dev.do(
            "note",
            scope="decision",
            task_id=tid,
            text=(
                "Verified every acceptance criterion on the branch: "
                + criteria
                + " — all hold against the committed content. Decision: no "
                "further changes needed; the file is self-contained."
            ),
        ),
        "dev during-work decision note",
    )
    expect_ok(
        dev.do(
            "note",
            text="Handoff summary below (section carries the content).",
            scope="handoff",
            task_id=tid,
            section={
                "summary": (
                    f"Built {filename} at the repo root on the task branch; "
                    "PR is open against the base branch; single additive "
                    "commit, no risks beyond trivial content review."
                )
            },
        ),
        "dev handoff section",
    )
    expect_ok(
        dev.do(
            "note",
            scope="reflect",
            task_id=tid,
            text=(
                "Reflection: implemented the task exactly per plan — wrote "
                "the file, committed on the task branch, opened the PR, and "
                "self-verified the acceptance criteria against the committed "
                "content."
            ),
        ),
        "dev reflect note",
    )
    expect_ok(dev.flow("i_am_done", task_id=tid), "dev i_am_done")
    assert task_state(stack, task_id)["status"] == "awaiting_qa"


def _criteria_text(stack: E2EStack, task_id: Any) -> str:
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> list[str]:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        return list(row.acceptance_criteria or [])

    crits: list[str] = stack.run_db(_run)
    return "; ".join(f'"{c}"' for c in crits)


def qa_arc(stack: E2EStack, company: Company, task_id: Any) -> None:
    """awaiting_qa → awaiting_documentation."""
    tid = str(task_id)
    qa = ScriptedAgent(stack, company.qa_id, "be-qa", "qa")
    expect_ok(qa.flow("claim_review", task_id=tid), "qa claim_review")
    expect_ok(
        qa.do(
            "note",
            scope="learning",
            task_id=tid,
            text=(
                "Review learning: the change is a single additive file; diff "
                "inspection on the PR confirms the acceptance criteria with "
                "no side effects on existing files."
            ),
        ),
        "qa learning note",
    )

    async def _crits(session: AsyncSession) -> list[str]:
        from roboco.db.tables import TaskTable
        from sqlalchemy import select

        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        return list(row.acceptance_criteria or [])

    criteria: list[str] = stack.run_db(_crits)
    expect_ok(
        qa.flow(
            "pass_review",
            task_id=tid,
            notes=(
                "Verified the PR diff on the origin: the committed change "
                "satisfies every acceptance criterion; no regressions in the "
                "diff, and the branch contains exactly the described commit."
            ),
            ac_verdicts=[
                f"{c} — verified against the PR diff on the origin." for c in criteria
            ],
        ),
        "qa pass_review",
    )
    assert task_state(stack, task_id)["status"] == "awaiting_documentation"


def doc_arc(stack: E2EStack, company: Company, task_id: Any, *, filename: str) -> None:
    """awaiting_documentation → awaiting_pm_review."""
    tid = str(task_id)
    doc = ScriptedAgent(stack, company.doc_id, "be-doc", "documenter")
    expect_ok(doc.flow("claim_doc_task", task_id=tid), "doc claim_doc_task")
    expect_ok(
        doc.flow(
            "i_documented",
            task_id=tid,
            files=[filename],
            notes=(
                f"Documented the change: {filename} carries the user-facing "
                "content; no API surface changed, README untouched by design."
            ),
        ),
        "doc i_documented",
    )
    state = task_state(stack, task_id)
    assert state["status"] == "awaiting_pm_review", state
    assert state["docs_complete"] is True, state


def seed_hierarchy(
    stack: E2EStack, company: Company, project_id: Any
) -> dict[str, Any]:
    """Root (Main-PM) → cell (cell-PM) → dev child, seeded mid-flight.

    Branch names follow the real convention (the task-short-id chain); the
    PM planning/delegation lane is a later scenario's subject.
    """
    from roboco.models import Team
    from roboco.models.base import TaskStatus, TaskType

    root_id = uuid4()
    cell_id = uuid4()
    root_branch = f"feature/backend/{str(root_id)[:8]}"
    cell_branch = f"{root_branch}--{str(cell_id)[:8]}"
    origin_branch(stack, root_branch, start="master")
    origin_branch(stack, cell_branch, start=root_branch)
    seed_task(
        stack,
        id=root_id,
        title="Delivery root: greeting program",
        description=(
            "Root coordination task assembling the greeting feature across "
            "the backend cell for the smoke harness merge-chain scenarios."
        ),
        acceptance_criteria=["the greeting feature lands on the root branch"],
        task_type=TaskType.PLANNING,
        # A delivery root belongs to the Main PM's lane — team routing
        # (closure, revision, reassignment) keys on this.
        team=Team.MAIN_PM,
        project_id=project_id,
        created_by=company.main_pm_id,
        assigned_to=company.main_pm_id,
        status=TaskStatus.IN_PROGRESS,
        branch_name=root_branch,
        active_claimant_id=company.main_pm_id,
    )
    seed_task(
        stack,
        id=cell_id,
        title="Backend slice: greeting file",
        description=(
            "Cell task assembling the backend slice of the greeting feature; "
            "one dev leaf writes the file, the cell PM assembles and submits."
        ),
        acceptance_criteria=["hello.txt exists at the repo root"],
        task_type=TaskType.PLANNING,
        project_id=project_id,
        created_by=company.main_pm_id,
        assigned_to=company.cell_pm_id,
        parent_task_id=root_id,
        status=TaskStatus.IN_PROGRESS,
        branch_name=cell_branch,
        active_claimant_id=company.cell_pm_id,
    )
    child_id = seed_task(
        stack,
        title="Write hello.txt",
        description=(
            "Create hello.txt with a friendly greeting at the repo root so "
            "the merge-chain scenario has a real change to assemble upward."
        ),
        acceptance_criteria=["hello.txt exists at the repo root"],
        project_id=project_id,
        created_by=company.cell_pm_id,
        parent_task_id=cell_id,
        assigned_to=company.dev_id,
    )
    return {
        "root_id": root_id,
        "root_branch": root_branch,
        "cell_id": cell_id,
        "cell_branch": cell_branch,
        "child_id": child_id,
    }


def seed_cell_and_dev(
    stack: E2EStack,
    company: Company,
    project_id: Any,
    root: dict[str, Any],
    *,
    filename: str,
) -> dict[str, Any]:
    """Cell (cell-PM) → dev child under an ALREADY-EXISTING root, seeded
    mid-flight — the cell/child half of :func:`seed_hierarchy`, generalized
    for a caller that owns its own root (e.g. a MegaTask root-subtask whose
    root is claimed/seeded separately from this cell). ``root`` carries
    ``root_id`` / ``root_branch`` (the shape :func:`seed_hierarchy` returns)."""
    from roboco.models.base import TaskStatus, TaskType

    root_id, root_branch = root["root_id"], root["root_branch"]
    cell_id = uuid4()
    cell_branch = f"{root_branch}--{str(cell_id)[:8]}"
    origin_branch(stack, cell_branch, start=root_branch)
    seed_task(
        stack,
        id=cell_id,
        title=f"Backend slice: {filename}",
        description=(
            "Cell task assembling the backend slice of the feature; one dev "
            "leaf writes the file, the cell PM assembles and submits."
        ),
        acceptance_criteria=[f"{filename} exists at the repo root"],
        task_type=TaskType.PLANNING,
        project_id=project_id,
        created_by=company.main_pm_id,
        assigned_to=company.cell_pm_id,
        parent_task_id=root_id,
        status=TaskStatus.IN_PROGRESS,
        branch_name=cell_branch,
        active_claimant_id=company.cell_pm_id,
    )
    child_id = seed_task(
        stack,
        title=f"Write {filename}",
        description=(
            f"Create {filename} with a friendly greeting at the repo root so "
            "the scenario has a real change to assemble upward."
        ),
        acceptance_criteria=[f"{filename} exists at the repo root"],
        project_id=project_id,
        created_by=company.cell_pm_id,
        parent_task_id=cell_id,
        assigned_to=company.dev_id,
    )
    return {
        "root_id": root_id,
        "root_branch": root_branch,
        "cell_id": cell_id,
        "cell_branch": cell_branch,
        "child_id": child_id,
    }


def reviewer_gate_pass_arc(stack: E2EStack, company: Company, task_id: Any) -> None:
    """awaiting_pr_review → awaiting_pm_review via the in-path gate."""
    reviewer = ScriptedAgent(
        stack, company.pr_reviewer_id, "pr-reviewer-1", "pr_reviewer"
    )
    expect_ok(
        reviewer.flow("claim_gate_review", task_id=str(task_id)),
        "reviewer claim_gate_review",
    )
    expect_ok(
        reviewer.do(
            "note",
            scope="learning",
            task_id=str(task_id),
            text=(
                "Gate review learning: the assembled diff is exactly the "
                "child's additive file with the integrity marker present; "
                "squash-merge assembly verified against the base branch."
            ),
        ),
        "reviewer learning note",
    )
    expect_ok(
        reviewer.flow(
            "pr_pass",
            task_id=str(task_id),
            notes=(
                "Assembled diff reviewed against the base branch: exactly the "
                "expected additive change, integrity markers present, no "
                "scope creep — passing to the PM for merge."
            ),
        ),
        "reviewer pr_pass",
    )
    assert task_state(stack, task_id)["status"] == "awaiting_pm_review"
