"""Colony orchestrator — main event loop for the multi-agent system."""

from __future__ import annotations

import asyncio
import time
from datetime import datetime, timezone
from pathlib import Path

import structlog
import yaml

from fliiq.runtime.agent.setup import AgentResources, setup_agent_resources
from fliiq.runtime.colony.agents import run_colony_agent
from fliiq.runtime.colony.config import next_experiment_id, save_colony_config
from fliiq.runtime.colony.console import (
    console,
    print_agent_done,
    print_agent_error,
    print_agent_start,
    print_code_freeze,
    print_experiment_header,
    print_governance_decision,
    print_metrics_summary,
    print_new_brief,
    print_new_proposal,
    print_new_vote,
    print_proposal_board,
    print_shutdown_summary,
)
from fliiq.runtime.colony.escalation import (
    notify_complete,
    notify_proposal_escalated,
    notify_started,
)
from fliiq.runtime.colony.events import (
    ColonyEvent,
    EventLog,
    EventPriority,
)
from fliiq.runtime.colony.git_ops import (
    commit_colony_state,
    create_experiment_branch,
    get_current_branch,
    switch_branch,
)
from fliiq.runtime.colony.models import (
    AgentRole,
    AgentTokenUsage,
    ColonyConfig,
    ColonyState,
    TimelineEntry,
)
from fliiq.runtime.colony.state import StateManager
from fliiq.runtime.llm.providers import LLMConfig

log = structlog.get_logger()


class Orchestrator:
    """Colony orchestrator — manages agent scheduling, events, and lifecycle."""

    def __init__(
        self,
        project_root: Path,
        colony_dir: Path,
        llm_config: LLMConfig,
        config: ColonyConfig,
    ):
        self._project_root = project_root
        self._colony_dir = colony_dir
        self._llm_config = llm_config
        self._config = config
        subdir = "missions" if config.mission else "state"
        self._state = StateManager(colony_dir, state_subdir=subdir)
        self._events = EventLog()
        self._running = False
        self._colony_state: ColonyState | None = None
        self._resources: AgentResources | None = None
        self._start_time: float = 0.0

        # Cooldown tracking: agent_name -> last_completed_monotonic_time
        self._last_run: dict[str, float] = {}
        self._cycle_count = 0
        self._parent_branch: str = ""

        # Stall detection for directed mode
        self._stall_count = 0
        self._last_artifact_snapshot: dict | None = None
        self._previous_context: str = ""

    async def start(self) -> None:
        """Full lifecycle: bootstrap -> main loop -> shutdown."""
        try:
            await self._bootstrap()
            await self._main_loop()
        except asyncio.CancelledError:
            log.info("colony.orchestrator_cancelled")
        except Exception as e:
            log.error("colony.orchestrator_error", error=str(e))
            raise
        finally:
            await self._shutdown()

    async def stop(self) -> None:
        """Signal the orchestrator to stop."""
        self._running = False

    # ── Bootstrap ──────────────────────────────────────────────────────

    async def _bootstrap(self) -> None:
        """Initialize colony: create branch, setup state, run initial cycles."""
        self._start_time = time.monotonic()

        # 1. Setup shared agent resources
        self._resources = await setup_agent_resources(
            mode="autonomous",
            llm_config=self._llm_config,
            project_root=self._project_root,
        )

        # 2. Create experiment branch (before writing files to avoid git conflicts)
        self._parent_branch = get_current_branch(self._project_root)
        exp_id = next_experiment_id(self._colony_dir)
        prefix = "mission" if self._config.mission else "experiment"
        branch_name = create_experiment_branch(self._project_root, exp_id, prefix=prefix)

        # 3. Initialize state directory (now on experiment branch)
        self._state.ensure_dirs()

        # 3b. Directed mode: clean slate + apply overrides
        if self._config.mission:
            # Read previous deliverable before clearing state
            reports_dir = self._colony_dir / "reports"
            deliverables = sorted(reports_dir.glob("mission-deliverable-*.md"))
            if deliverables:
                self._previous_context = deliverables[-1].read_text()
                log.info(
                    "colony.previous_deliverable_loaded",
                    path=str(deliverables[-1].name),
                )
            self._state.reset_working_state()

        save_colony_config(self._colony_dir, self._config)

        if self._config.mission:
            self._config.cooldowns = dict(self._config.directed_cooldowns)
            self._config.max_iterations_per_cycle = self._config.directed_max_iterations
            log.info(
                "colony.directed_mode",
                mission=self._config.mission[:100],
                cooldowns=self._config.cooldowns,
                max_iterations=self._config.max_iterations_per_cycle,
            )

        # 4. Save initial colony state
        self._colony_state = ColonyState(
            experiment_id=exp_id,
            branch_name=branch_name,
            mission=self._config.mission,
        )
        self._state.save_colony_state(self._colony_state)
        commit_colony_state(
            self._project_root, "[colony] Initialize experiment",
        )

        # 5. Show header
        print_experiment_header(
            self._colony_state, 0.0,
            self._config.experiment_duration_hours,
        )

        # 6. Notify via Telegram
        if self._config.telegram_chat_id:
            await notify_started(
                self._config.telegram_chat_id,
                branch_name,
                self._config.experiment_duration_hours,
                mission=self._config.mission,
            )

        # 7. Sequential bootstrap: Intel -> Scout -> Research -> QA -> Gov
        if self._config.mission:
            bootstrap_order = self._directed_bootstrap_order()
        else:
            bootstrap_order = self._auto_bootstrap_order()

        for role, trigger in bootstrap_order:
            await self._run_agent_safe(
                role, 0, f"[Bootstrap] {trigger}",
            )

        # Commit bootstrap state
        commit_colony_state(
            self._project_root, "[colony] Bootstrap complete",
        )

        self._running = True
        self._colony_state.status = "running"
        self._state.save_colony_state(self._colony_state)

        # Show proposal board after bootstrap
        self._show_proposal_board()

    # ── Main Loop ──────────────────────────────────────────────────────

    async def _main_loop(self) -> None:
        """Event-driven main loop with cooldown-based scheduling."""
        duration_secs = self._config.experiment_duration_hours * 3600
        freeze_secs = self._config.code_freeze_at_hour * 3600

        while self._running:
            elapsed = time.monotonic() - self._start_time

            # Check experiment duration
            if elapsed >= duration_secs:
                break

            # Check code freeze
            in_freeze = elapsed >= freeze_secs
            if (
                in_freeze
                and self._colony_state
                and self._colony_state.status != "code_freeze"
            ):
                self._colony_state.status = "code_freeze"
                self._state.save_colony_state(self._colony_state)
                print_code_freeze(elapsed / 3600)
                self._log_timeline(
                    "orchestrator", "Code freeze activated",
                    f"Hour {elapsed / 3600:.1f}",
                )

            # 1. Detect new events from state files
            await self._detect_events()

            # 2. Process high-priority events
            await self._process_events()

            # 3. Schedule agents based on cooldowns
            await self._schedule_agents(in_freeze)

            # 4. Periodically commit state + show dashboard
            if self._cycle_count > 0 and self._cycle_count % 5 == 0:
                commit_colony_state(
                    self._project_root,
                    f"[colony] Checkpoint (cycle {self._cycle_count})",
                )
                if self._colony_state:
                    print_experiment_header(
                        self._colony_state,
                        elapsed / 3600,
                        self._config.experiment_duration_hours,
                    )
                self._show_proposal_board()

            # 5. Directed mode: check governance completion declaration
            if self._config.mission and self._check_mission_complete():
                self._log_timeline(
                    "orchestrator", "Mission declared complete by Governance",
                )
                break

            # 6. Directed mode: stall detection
            if self._config.mission and self._check_stall():
                self._log_timeline(
                    "orchestrator", "Stall detected — no new artifacts for 3 rotations",
                )
                break

            # 7. Sleep before next iteration
            await asyncio.sleep(30)

    # ── Event Detection ────────────────────────────────────────────────

    async def _detect_events(self) -> None:
        """Poll state files for new proposals needing governance review."""
        proposals = self._state.list_proposals(status="proposed")
        for p in proposals:
            self._events.push(ColonyEvent(
                priority=EventPriority.GOVERNANCE_REVIEW,
                event_type="proposal_submitted",
                target_agent="governance",
                payload={"proposal_id": p.id, "title": p.title},
            ))
            self._state.update_proposal_status(p.id, "under_review")
            self._log_timeline(
                "orchestrator",
                f"Queued {p.id} for governance review",
                p.title,
            )

        # Check for QA regression signals
        metrics = self._state.get_latest_metrics()
        if metrics and metrics.fail_count > 0:
            self._events.push(ColonyEvent(
                priority=EventPriority.QA_REGRESSION,
                event_type="qa_regression",
                target_agent="qa",
                payload={"fail_count": metrics.fail_count},
            ))

        # Check for escalated proposals needing notification
        escalated = self._state.list_proposals(status="escalated")
        for p in escalated:
            if self._config.telegram_chat_id:
                await notify_proposal_escalated(
                    self._config.telegram_chat_id,
                    p.id,
                    p.title,
                    f"Risk: {p.risk}, Type: {p.type}",
                )

    # ── Event Processing ───────────────────────────────────────────────

    async def _process_events(self) -> None:
        """Process pending events by dispatching to appropriate agents."""
        processed = 0
        while self._events.pending_count > 0 and processed < 3:
            event = self._events.pop()
            if event is None:
                break

            if event.target_agent == "governance":
                trigger = (
                    f"Review event: {event.event_type}. "
                    f"{event.payload}"
                )
                await self._run_agent_safe(
                    AgentRole.GOVERNANCE,
                    self._cycle_count,
                    trigger,
                )
                processed += 1

            elif (
                event.target_agent == "qa"
                and event.event_type == "qa_regression"
            ):
                fails = event.payload.get("fail_count", 0)
                trigger = (
                    f"Regression detected: {fails} test failures."
                    " Investigate and propose fix."
                )
                await self._run_agent_safe(
                    AgentRole.QA, self._cycle_count, trigger,
                )
                processed += 1

    # ── Agent Scheduling ───────────────────────────────────────────────

    async def _schedule_agents(self, in_freeze: bool) -> None:
        """Run agents whose cooldowns have elapsed."""
        if in_freeze:
            if self._is_cooled_down("qa"):
                await self._run_agent_safe(
                    AgentRole.QA, self._cycle_count,
                    "Code freeze monitoring. Run tests and report.",
                )
            return

        if self._config.mission:
            scanning_agents = self._directed_scanning_agents()
        else:
            scanning_agents = self._auto_scanning_agents()

        for role, default_trigger in scanning_agents:
            if self._is_cooled_down(role.value):
                await self._run_agent_safe(
                    role, self._cycle_count, default_trigger,
                )

    def _is_cooled_down(self, agent: str) -> bool:
        """Check if an agent's cooldown has elapsed."""
        last = self._last_run.get(agent, 0)
        cooldown = self._config.cooldowns.get(agent, 900)
        return (time.monotonic() - last) >= cooldown

    # ── Auto/Directed Trigger Lists ───────────────────────────────────

    def _auto_bootstrap_order(self) -> list[tuple[AgentRole, str]]:
        return [
            (AgentRole.INTELLIGENCE, (
                "Perform initial AI landscape scan. Find recent"
                " developments relevant to Fliiq — an AI agent"
                " framework for task automation."
            )),
            (AgentRole.SCOUT, (
                "Perform initial demand sensing. Scan for user needs,"
                " feature requests, and skill gaps in the AI agent"
                " space."
            )),
            (AgentRole.RESEARCH, (
                "Review intelligence briefs and scout findings."
                " Identify immediate improvement opportunities"
                " for Fliiq."
            )),
            (AgentRole.QA, (
                "Run baseline health check: pytest tests/ -q and"
                " ruff check fliiq/. Record initial metrics to"
                " establish baseline."
            )),
            (AgentRole.GOVERNANCE, (
                "Review any initial proposals. Establish baseline"
                " governance posture. Evaluate and decide on"
                " pending proposals."
            )),
        ]

    def _directed_bootstrap_order(self) -> list[tuple[AgentRole, str]]:
        m = self._config.mission
        return [
            (AgentRole.INTELLIGENCE, (
                f"Mission: {m}\n"
                "Research existing work, tools, and prior art relevant"
                " to this mission. Find what already exists."
            )),
            (AgentRole.SCOUT, (
                f"Mission: {m}\n"
                "Break the mission into concrete deliverables. Write"
                " each as a formal proposal."
            )),
            (AgentRole.RESEARCH, (
                f"Mission: {m}\n"
                "Review scout proposals. Draft detailed implementations"
                " for the first batch of deliverables."
            )),
            (AgentRole.QA, (
                "Run baseline health check: pytest tests/ -q and"
                " ruff check fliiq/. Record initial metrics."
            )),
            (AgentRole.GOVERNANCE, (
                f"Mission: {m}\n"
                "Review initial proposals. Prioritize by mission impact."
                " Approve and implement highest-value items first."
            )),
        ]

    def _auto_scanning_agents(self) -> list[tuple[AgentRole, str]]:
        return [
            (AgentRole.INTELLIGENCE, (
                "Scan AI landscape for new developments"
                " relevant to Fliiq."
            )),
            (AgentRole.SCOUT, (
                "Scan for user demands, feature opportunities,"
                " and skill gaps."
            )),
            (AgentRole.RESEARCH, (
                "Evaluate recent intelligence briefs. Propose"
                " improvements if warranted."
            )),
            (AgentRole.QA, (
                "Run health checks: pytest and ruff. Report"
                " metrics. Vote on pending proposals."
            )),
        ]

    def _directed_scanning_agents(self) -> list[tuple[AgentRole, str]]:
        m = self._config.mission
        return [
            (AgentRole.INTELLIGENCE, (
                f"Mission: {m}\n"
                "Find new developments, tools, or resources"
                " relevant to the mission."
            )),
            (AgentRole.SCOUT, (
                f"Mission: {m}\n"
                "Review what has been completed. Identify next"
                " batch of deliverables and write proposals."
            )),
            (AgentRole.RESEARCH, (
                f"Mission: {m}\n"
                "Review recent proposals and intelligence. Draft"
                " new proposals for remaining deliverables."
            )),
            (AgentRole.QA, (
                "Run health checks: pytest and ruff. Report"
                " metrics. Vote on pending proposals."
            )),
        ]

    # ── Directed Mode: Completion Detection ───────────────────────────

    def _check_mission_complete(self) -> bool:
        """Check if Governance declared mission complete via mission_status.yaml."""
        status_file = self._colony_dir / "state" / "mission_status.yaml"
        if not status_file.is_file():
            return False
        try:
            data = yaml.safe_load(status_file.read_text()) or {}
            if data.get("mission_complete"):
                log.info(
                    "colony.mission_complete",
                    rationale=data.get("rationale", ""),
                    summary=data.get("summary", ""),
                )
                return True
        except Exception:
            pass
        return False

    def _check_stall(self) -> bool:
        """Detect if no new artifacts were produced for 3 consecutive checks."""
        current = self._count_artifacts()
        if self._last_artifact_snapshot is None:
            self._last_artifact_snapshot = current
            return False

        if current == self._last_artifact_snapshot:
            self._stall_count += 1
            if self._stall_count >= 3:
                log.info("colony.stall_detected", stall_count=self._stall_count)
                return True
        else:
            self._stall_count = 0
            self._last_artifact_snapshot = current

        return False

    def _count_artifacts(self) -> dict:
        """Count current artifact totals for stall detection."""
        return {
            "proposals": len(self._state.list_proposals()),
            "briefs": len(self._state.list_intel_briefs()),
            "decisions": len(self._state.get_decisions()),
            "votes": sum(
                len(self._state.get_votes_for(p.id))
                for p in self._state.list_proposals()
            ),
        }

    # ── Agent Execution ────────────────────────────────────────────────

    async def _run_agent_safe(
        self, role: AgentRole, cycle: int, trigger: str,
    ) -> None:
        """Run agent with state diffing, display, and error handling."""
        print_agent_start(role.value, trigger)

        # Snapshot state before agent runs
        before = self._snapshot_state()

        try:
            result = await run_colony_agent(
                role=role,
                llm=self._resources.llm,
                tools=self._resources.tools,
                state=self._state,
                project_root=self._project_root,
                trigger_context=trigger,
                cycle_number=cycle,
                max_iterations=self._config.max_iterations_per_cycle,
                mission=self._config.mission,
                previous_context=self._previous_context,
            )

            print_agent_done(role.value, result.iterations)

            # Track token usage
            self._state.append_token_usage(AgentTokenUsage(
                agent=role.value,
                cycle_number=cycle,
                input_tokens=result.input_tokens,
                output_tokens=result.output_tokens,
            ))

            # Log to timeline
            summary = (
                result.final_text[:200]
                if result.final_text
                else f"Completed in {result.iterations} iterations"
            )
            self._log_timeline(role.value, summary, trigger[:100])

        except Exception as e:
            log.error(
                "colony.agent_failed",
                agent=role.value, cycle=cycle, error=str(e),
            )
            print_agent_error(role.value, str(e))
            self._log_timeline(
                role.value, f"ERROR: {e!s}", trigger[:100],
            )

        finally:
            self._last_run[role.value] = time.monotonic()
            self._cycle_count += 1
            if self._colony_state:
                self._colony_state.cycle_count = self._cycle_count
                self._state.save_colony_state(self._colony_state)

        # Announce new artifacts created during this agent's run
        self._announce_new_artifacts(before)

    def _snapshot_state(self) -> dict:
        """Capture current artifact IDs before an agent run."""
        return {
            "proposal_ids": {
                p.id for p in self._state.list_proposals()
            },
            "brief_ids": {
                b.id for b in self._state.list_intel_briefs()
            },
            "decision_count": len(self._state.get_decisions()),
        }

    def _announce_new_artifacts(self, before: dict) -> None:
        """Diff state after agent run and announce new items."""
        # New intelligence briefs
        for brief in self._state.list_intel_briefs():
            if brief.id not in before["brief_ids"]:
                print_new_brief(brief)

        # New proposals
        all_proposals = self._state.list_proposals()
        for p in all_proposals:
            if p.id not in before["proposal_ids"]:
                print_new_proposal(p)

        # New votes (check all proposals for new votes)
        for p in all_proposals:
            for v in self._state.get_votes_for(p.id):
                # Only announce if proposal existed before but vote is new
                if p.id in before["proposal_ids"]:
                    print_new_vote(v)
                    break  # one announcement per proposal per cycle

        # New governance decisions
        decisions = self._state.get_decisions()
        new_count = len(decisions) - before["decision_count"]
        if new_count > 0:
            for d in decisions[-new_count:]:
                print_governance_decision(d)

        # Latest metrics
        metrics = self._state.get_latest_metrics()
        if metrics:
            print_metrics_summary(metrics)

    def _show_proposal_board(self) -> None:
        """Display the proposal status table."""
        proposals = self._state.list_proposals()
        if not proposals:
            return
        votes_map = {}
        for p in proposals:
            votes_map[p.id] = len(self._state.get_votes_for(p.id))
        print_proposal_board(proposals, votes_map)

    # ── Timeline ───────────────────────────────────────────────────────

    def _log_timeline(
        self, agent: str, action: str, detail: str = "",
    ) -> None:
        entry = TimelineEntry(
            time=datetime.now(timezone.utc),
            agent=agent,
            action=action,
            detail=detail,
        )
        self._state.append_timeline(entry)

    # ── Shutdown ───────────────────────────────────────────────────────

    async def _shutdown(self) -> None:
        """Generate final report, commit, and switch back to parent branch."""
        if self._colony_state:
            self._colony_state.status = "stopped"

            all_proposals = self._state.list_proposals()
            self._colony_state.proposals_total = len(all_proposals)
            self._colony_state.proposals_approved = len(
                [
                    p for p in all_proposals
                    if p.status.value in ("approved", "implemented")
                ],
            )
            self._colony_state.proposals_rejected = len(
                [
                    p for p in all_proposals
                    if p.status.value == "rejected"
                ],
            )
            self._state.save_colony_state(self._colony_state)

        # Show final proposal board
        self._show_proposal_board()

        # Generate session report + mission deliverable
        try:
            from fliiq.runtime.colony.reporting import ReportGenerator

            gen = ReportGenerator(self._state, self._project_root)
            exp_id = self._colony_state.experiment_id if self._colony_state else 0
            report_path = gen.generate_session_report(exp_id)
            console.print(f"  Report: {report_path}")

            if self._config.mission:
                deliverable_path = gen.generate_mission_deliverable(
                    exp_id, self._config.mission,
                )
                console.print(f"  Deliverable: {deliverable_path}")
        except Exception as e:
            log.error("colony.report_failed", error=str(e))

        # Final commit
        try:
            commit_colony_state(
                self._project_root,
                "[colony] Experiment complete — final state",
            )
        except Exception as e:
            log.warning("colony.final_commit_failed", error=str(e))

        # Notify summary
        summary = ""
        if self._colony_state:
            mission_line = (
                f"Mission: {self._config.mission}\n"
                if self._config.mission else ""
            )
            summary = (
                f"Experiment {self._colony_state.experiment_id}\n"
                f"{mission_line}"
                f"Cycles: {self._colony_state.cycle_count}\n"
                f"Proposals: {self._colony_state.proposals_total} total, "
                f"{self._colony_state.proposals_approved} approved, "
                f"{self._colony_state.proposals_rejected} rejected"
            )

        if self._config.telegram_chat_id:
            await notify_complete(
                self._config.telegram_chat_id, summary,
            )

        # Switch back to parent branch
        target = self._parent_branch or "main"
        try:
            current = get_current_branch(self._project_root)
            if current != target:
                switch_branch(self._project_root, target)
                console.print(f"  [dim]Switched back to {target}[/dim]")
        except Exception as e:
            log.warning("colony.branch_switch_failed", error=str(e))

        # Final summary panel
        if self._colony_state:
            print_shutdown_summary(self._colony_state)
