use std::any::Any;

use thiserror::Error;

use crate::agent_pty::TabMembership;
use crate::event::AgentType;

#[derive(Debug, Error)]
pub enum PaneError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Command failed: {0}")]
    CommandFailed(String),
    #[error("Parse error: {0}")]
    ParseError(String),
    #[error("Pane control not available")]
    NotAvailable,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaneDirection {
    Up,
    Down,
    Left,
    Right,
}

#[derive(Debug, Clone)]
pub struct PaneInfo {
    pub pane_id: String,
    pub title: String,
    pub is_focused: bool,
    pub command: Option<String>,
}

/// PRD #92 F4: per-pane result of a multi-pane close operation (a mode
/// tab teardown via [`crate::tab::TabManager::close_tab`], or a mode
/// deactivation via [`crate::mode_manager::ModeManager::deactivate_mode`]).
///
/// Pre-F4 the same operations returned `Result<Vec<String>, _>` and
/// silently dropped any [`PaneController::close_pane`] errors. That
/// produced the user-visible bug where pressing `Ctrl+W` on an
/// unhealthy mode-tab destroyed the dashboard cards while the underlying
/// agent processes survived in the daemon registry — the TUI thought
/// they were gone.
///
/// Callers consume the outcome like this: `closed` is the set of pane
/// IDs whose `close_pane` returned `Ok(())` — the UI should remove
/// those sessions, cards, and pane metadata as before. `failed` is the
/// set of `(pane_id, rendered_error)` pairs — the UI keeps those
/// cards / sessions, restores the local pane state (the controller
/// already did this on its `Err` path), and surfaces the errors in
/// `ui.status_message` so the user can retry.
///
/// The error half is rendered to `String` rather than carried as
/// [`PaneError`] because [`PaneError::Io`] wraps [`std::io::Error`]
/// which is not `Clone`. The caller renders errors into the status
/// message anyway, so collecting strings is what we already want.
#[derive(Debug, Default)]
pub struct CloseTabOutcome {
    /// Pane IDs whose `close_pane` returned `Ok(())`.
    pub closed: Vec<String>,
    /// Pane IDs whose `close_pane` returned `Err`, paired with the
    /// rendered error message (`format!("{}", e)`).
    pub failed: Vec<(String, String)>,
}

impl CloseTabOutcome {
    /// Record one pane's close result. `Ok(())` pushes to `closed`;
    /// `Err(e)` pushes `(pane_id, e.to_string())` to `failed`.
    pub fn record(&mut self, pane_id: String, result: Result<(), PaneError>) {
        match result {
            Ok(()) => self.closed.push(pane_id),
            Err(e) => self.failed.push((pane_id, e.to_string())),
        }
    }

    /// True iff every `close_pane` call in the outcome returned `Ok`.
    pub fn is_clean(&self) -> bool {
        self.failed.is_empty()
    }
}

/// Outcome of a [`PaneController::rename_pane`] call.
///
/// M2.11 fixup 5 — the rename path now returns what the controller
/// actually did with the user-supplied text, so callers (the dashboard
/// rename handler in particular) can mirror the EXACT label the
/// controller stored on `Pane.name` (and queued for the daemon) into
/// `ui.display_names` / `ui.pane_display_names`. Before this, the UI
/// inserted the raw `ui.rename_text` verbatim, which diverged from
/// the controller's trim + `is_valid_display_name` normalization (a
/// `"  newname  "` rename left the dashboard map padded while the
/// daemon stored `"newname"`, and a control-byte rename slipped
/// terminal escapes into the dashboard card title even though the
/// controller refused the change). This extends fixup 4's "single
/// source of truth" pattern from create to rename.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenameOutcome {
    /// Controller accepted the rename; the included string is the
    /// canonical label now stored on local `Pane.name` (and, for
    /// stream-backed panes, queued for the daemon via
    /// `set_agent_label`). Callers must mirror this exact string
    /// into their UI display-name maps.
    ///
    /// **Invariant**: the inner `String` must be the trimmed value of
    /// the user's input and must satisfy
    /// [`crate::agent_pty::is_valid_display_name`]. The only correct
    /// way to construct this variant outside a context that has just
    /// validated the label itself (the production controller, or a
    /// test using a statically known-valid literal) is via
    /// [`RenameOutcome::applied`] — which trims, validates, and falls
    /// back to [`RenameOutcome::Cleared`] / [`RenameOutcome::Rejected`]
    /// for invalid input. Bypassing the constructor with raw user
    /// input would allow padded or control-byte labels into UI
    /// display-name maps and break the fixup-5 normalization
    /// guarantee (PRD #76 M2.11 fixup-5 auditor LOW).
    Applied(String),
    /// Controller treated the rename as a "clear" — empty or
    /// whitespace-only input. Local `Pane.name` is now empty and the
    /// daemon (if any) will be sent `display_name: None` so hydrate
    /// falls back to the agent_id on reconnect rather than restoring
    /// a stale label. Callers should remove the corresponding entry
    /// from their UI display-name maps.
    Cleared,
    /// Controller refused the rename because the trimmed value
    /// failed `is_valid_display_name` (control bytes, oversized,
    /// etc.). Neither local `Pane.name` nor the daemon was mutated;
    /// callers must leave their UI display-name maps unchanged so
    /// the previous label stays visible.
    Rejected,
}

impl RenameOutcome {
    /// Construct a [`RenameOutcome`] from raw user-supplied rename text,
    /// applying the same trim + validation rules the production
    /// controller (`EmbeddedPaneController::rename_pane`) uses. This is
    /// the single canonical way to derive a `RenameOutcome` from
    /// untrusted input: it guarantees the [`Applied`](Self::Applied)
    /// invariant by routing invalid bytes to [`Rejected`](Self::Rejected)
    /// and empty/whitespace-only input to [`Cleared`](Self::Cleared).
    ///
    /// Mock `PaneController::rename_pane` implementations should call
    /// this instead of constructing `Applied(name.to_string())` directly
    /// — otherwise a test driving invalid input through a mock would
    /// produce an `Applied` value that violates the documented variant
    /// invariant and could falsely prove the UI accepts unvalidated
    /// labels (PRD #76 M2.11 fixup-5 auditor LOW).
    ///
    /// Semantics, matching the production controller:
    /// - Trim the input.
    /// - Empty after trim → [`Cleared`](Self::Cleared).
    /// - Non-empty + [`crate::agent_pty::is_valid_display_name`] →
    ///   [`Applied`](Self::Applied) with the trimmed label.
    /// - Non-empty + invalid → [`Rejected`](Self::Rejected).
    pub fn applied(name: impl AsRef<str>) -> Self {
        let trimmed = name.as_ref().trim();
        if trimmed.is_empty() {
            Self::Cleared
        } else if crate::agent_pty::is_valid_display_name(trimmed) {
            Self::Applied(trimmed.to_string())
        } else {
            Self::Rejected
        }
    }
}

/// PRD #76 M2.12 spawn-time options bundled into one struct so the
/// [`PaneController::create_pane_with_options`] trait method doesn't grow
/// a six-arg signature. `display_name` and `tab_membership` are forwarded
/// to the daemon-side `StartAgent` payload so a reconnect rehydrates the
/// pane with its label and tab home intact.
///
/// Field semantics:
/// - `display_name`: user-supplied label (from the new-pane form, or a
///   role name in an orchestration). Resolved via
///   [`crate::agent_pty::resolve_display_name`] before reaching the daemon.
/// - `tab_membership`: which tab this pane belonged to. `None` means
///   dashboard pane — same default as the legacy
///   `create_pane_with_display_name` path.
/// - `rows` / `cols`: initial PTY dimensions (PRD #76 M2.15). Required —
///   the TUI always knows its viewport, and an `Option` that's never `None`
///   in practice would invite drift. Callers compute the value at the spawn
///   site using the same layout-math helpers the resize-time path calls
///   (`agent_pane_dims_*` in `ui.rs`), so the agent's first frame draws at
///   the eventual size instead of the legacy 24×80 default + immediate
///   resize hiccup. `Default` returns the conservative 24/80 fallback so
///   test fixtures using `..Default::default()` continue to compile.
#[derive(Debug, Clone)]
pub struct AgentSpawnOptions<'a> {
    pub display_name: Option<&'a str>,
    pub tab_membership: Option<TabMembership>,
    pub rows: u16,
    pub cols: u16,
    /// PRD #76 M2.13: which AI agent this spawn command runs (typically
    /// inferred at the call site via [`AgentType::from_command`]).
    /// Forwarded to the daemon via `StartAgentOptions.agent_type` so the
    /// registry's `agent_type` echo on `list_agents` can seed hydrated
    /// placeholder sessions with the correct type. `None` means
    /// "unknown / not an agent" — same default as the legacy path.
    pub agent_type: Option<AgentType>,
}

impl Default for AgentSpawnOptions<'_> {
    fn default() -> Self {
        Self {
            display_name: None,
            tab_membership: None,
            // Conservative VT100 fallback — production callers in
            // `src/ui.rs` always set both explicitly via the layout
            // helpers; defaults exist only for test fixtures.
            rows: 24,
            cols: 80,
            agent_type: None,
        }
    }
}

pub trait PaneController: Send + Sync {
    fn focus_pane(&self, pane_id: &str) -> Result<(), PaneError>;
    /// PRD #76 M2.15 fixup pass 2 G1 — legacy single-call entry point.
    /// The default impl routes through `create_pane_with_options` with
    /// `AgentSpawnOptions::default()`, which opens the PTY at the
    /// conservative 24×80 fallback. **Production callers must NOT use
    /// this method**: every production spawn now goes through
    /// `create_pane_with_options` with real dims computed via the
    /// `*_pane_dims` SSOT helpers in `ui.rs` (`dashboard_pane_dims`,
    /// `mode_agent_pane_dims`, `mode_side_pane_dims`,
    /// `orchestration_role_pane_dims`). After the M2.15 fixup pass 2,
    /// the legacy `EmbeddedPaneController::create_pane` override was
    /// deleted so no production controller can hit the 24×80 fallback —
    /// only test mocks (which intentionally override `create_pane` to
    /// avoid wiring the full `create_pane_with_options` shape) and test
    /// fixtures that call `.create_pane(...)` directly still route
    /// through this method.
    ///
    /// **Recursion note:** both `create_pane` and `create_pane_with_options`
    /// have default impls that call into each other. Every concrete
    /// `PaneController` impl must override at least one to break the
    /// cycle: `EmbeddedPaneController` overrides `create_pane_with_options`;
    /// test mocks override `create_pane`. A new impl that overrides
    /// neither will infinitely recurse at runtime on the first
    /// `create_pane*` call.
    fn create_pane(&self, command: Option<&str>, cwd: Option<&str>) -> Result<String, PaneError> {
        self.create_pane_with_options(command, cwd, AgentSpawnOptions::default())
            .map(|(id, _)| id)
    }
    /// Create a pane and apply a display name in one step. The default impl
    /// composes `create_pane` + `rename_pane`, which is fine for local-PTY
    /// backends. The stream-backed `EmbeddedPaneController` overrides this
    /// so the name reaches the daemon via `StartAgent.display_name` —
    /// otherwise a disconnect or crash between `create_pane` and
    /// `rename_pane` would persist the command-based fallback name on the
    /// daemon and lose the user's chosen Name on the next reconnect
    /// (PRD #76 M2.11 reviewer P2).
    ///
    /// Returns `(pane_id, resolved_display_name)` so the caller can mirror
    /// the EXACT label the controller (and the daemon, for stream panes)
    /// stored. The resolved name is computed via
    /// [`crate::agent_pty::resolve_display_name`] — the single source of
    /// truth shared with the UI maps, preventing the divergence between
    /// `ui.pane_display_names` and `AgentRecord.display_name` that fixup-3
    /// reviewer P2 / auditor LOW called out.
    fn create_pane_with_display_name(
        &self,
        command: Option<&str>,
        cwd: Option<&str>,
        display_name: Option<&str>,
    ) -> Result<(String, String), PaneError> {
        self.create_pane_with_options(
            command,
            cwd,
            AgentSpawnOptions {
                display_name,
                ..Default::default()
            },
        )
    }
    /// PRD #76 M2.12: superset of `create_pane_with_display_name` that
    /// also carries `tab_membership` through to the daemon's `StartAgent`
    /// payload. Mode and orchestration tab callers use this so a reconnect
    /// can rebuild tab structure instead of stranding every hydrated pane
    /// on the dashboard. Dashboard callers pass `tab_membership: None` and
    /// see the legacy behavior.
    ///
    /// The default impl mirrors the create-then-rename pattern of
    /// `create_pane_with_display_name` and ignores `tab_membership` — fine
    /// for local-PTY mocks where there's no daemon to echo the field. The
    /// stream-backed `EmbeddedPaneController` overrides this so the value
    /// reaches the daemon via `StartAgent.tab_membership`.
    fn create_pane_with_options(
        &self,
        command: Option<&str>,
        cwd: Option<&str>,
        opts: AgentSpawnOptions<'_>,
    ) -> Result<(String, String), PaneError> {
        let resolved = crate::agent_pty::resolve_display_name(opts.display_name, command);
        let id = self.create_pane(command, cwd)?;
        // Discard the RenameOutcome here: `resolved` is already the
        // canonical resolved value (computed via the shared resolver),
        // so the controller's outcome adds no new information for the
        // create-then-rename path. The new outcome shape matters at
        // the dashboard rename call site, not here.
        self.rename_pane(&id, &resolved)?;
        Ok((id, resolved))
    }
    fn close_pane(&self, pane_id: &str) -> Result<(), PaneError>;
    fn list_panes(&self) -> Result<Vec<PaneInfo>, PaneError>;
    fn resize_pane(
        &self,
        pane_id: &str,
        direction: PaneDirection,
        amount: u16,
    ) -> Result<(), PaneError>;
    /// Rename a pane. Returns a [`RenameOutcome`] describing what the
    /// controller actually did so callers (the dashboard rename
    /// handler in particular) can mirror the controller-resolved
    /// label into their UI display-name maps instead of inserting
    /// the raw user input verbatim. See [`RenameOutcome`] for the
    /// three states.
    fn rename_pane(&self, pane_id: &str, name: &str) -> Result<RenameOutcome, PaneError>;
    fn toggle_layout(&self) -> Result<(), PaneError>;
    fn write_to_pane(&self, pane_id: &str, text: &str) -> Result<(), PaneError>;
    fn name(&self) -> &str;
    fn is_available(&self) -> bool;
    fn as_any(&self) -> &dyn Any;
}

#[cfg(test)]
mod tests {
    use super::*;

    // M2.11 fixup 6 — pin the typed `RenameOutcome::applied` constructor
    // semantics. These tests are what makes the constructor a meaningful
    // type-level guarantee: callers (production controller + mocks) can
    // route raw user input through one function and trust the three
    // documented outcomes, instead of repeating the trim + validation
    // dance and risking divergence (PRD #76 M2.11 fixup-5 auditor LOW).

    #[test]
    fn rename_outcome_applied_returns_applied_for_valid_input() {
        assert_eq!(
            RenameOutcome::applied("foo"),
            RenameOutcome::Applied("foo".to_string())
        );
    }

    #[test]
    fn rename_outcome_applied_trims_surrounding_whitespace() {
        // Matches the production controller's trim semantics so the
        // dashboard map can't end up storing a padded label.
        assert_eq!(
            RenameOutcome::applied("  foo  "),
            RenameOutcome::Applied("foo".to_string())
        );
        assert_eq!(
            RenameOutcome::applied("\tbar\n"),
            RenameOutcome::Applied("bar".to_string())
        );
    }

    #[test]
    fn rename_outcome_applied_rejects_control_bytes() {
        // ANSI ESC after trim — the canonical "garbage that would slip
        // into the dashboard title" case. Must NOT become Applied.
        assert_eq!(RenameOutcome::applied("\x1b[31m"), RenameOutcome::Rejected);
        assert_eq!(
            RenameOutcome::applied("  \x1b[31mevil  "),
            RenameOutcome::Rejected
        );
    }

    #[test]
    fn rename_outcome_applied_treats_empty_as_cleared() {
        assert_eq!(RenameOutcome::applied(""), RenameOutcome::Cleared);
    }

    #[test]
    fn rename_outcome_applied_treats_whitespace_only_as_cleared() {
        // User's "clear" intent — must map to Cleared so the daemon-side
        // field is cleared rather than stored as a blank label.
        assert_eq!(RenameOutcome::applied("   "), RenameOutcome::Cleared);
        assert_eq!(RenameOutcome::applied("\t \n"), RenameOutcome::Cleared);
    }

    #[test]
    fn rename_outcome_applied_rejects_oversized_label() {
        // Mirror the daemon-side cap (`is_valid_display_name` rejects
        // > DISPLAY_NAME_MAX_LEN = 128 bytes after trim). A 129-byte
        // payload must surface as Rejected so the UI never mirrors a
        // label the daemon would refuse to store — otherwise the
        // dashboard map and `AgentRecord.display_name` would diverge.
        let oversized = "a".repeat(crate::agent_pty::DISPLAY_NAME_MAX_LEN + 1);
        assert_eq!(RenameOutcome::applied(&oversized), RenameOutcome::Rejected);
    }

    #[test]
    fn rename_outcome_applied_accepts_unicode_label() {
        // `is_valid_display_name` allows bytes ≥ 0x20 (control bytes
        // and DEL are the only thing filtered), so legitimate UTF-8
        // labels — kanji, emoji, accented Latin — must round-trip as
        // Applied with the trimmed bytes preserved. Pads with
        // surrounding whitespace to exercise the trim step too.
        assert_eq!(
            RenameOutcome::applied("  café-агент-日本語  "),
            RenameOutcome::Applied("café-агент-日本語".to_string())
        );
    }
}
