use std::fmt;

use crate::{BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
    Local,
    Backend,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedTarget {
    Local,
    Backend {
        base_url: String,
        workspace_id: String,
    },
}

impl ResolvedTarget {
    pub fn kind(&self) -> TargetKind {
        match self {
            Self::Local => TargetKind::Local,
            Self::Backend { .. } => TargetKind::Backend,
        }
    }
}

impl fmt::Display for TargetKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Local => f.write_str("local"),
            Self::Backend => f.write_str("Backend"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalTarget;

impl LocalTarget {
    pub fn new() -> Self {
        Self
    }

    fn runtime_command(&self) -> Result<WorkerRuntimeCommand, TargetError> {
        WorkerRuntimeCommand::resolve().map_err(TargetError::local_runtime_command)
    }
}

impl Default for LocalTarget {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendTarget {
    pub base_url: String,
    pub workspace_id: Option<String>,
}

impl BackendTarget {
    pub fn new(base_url: impl Into<String>, workspace_id: Option<impl Into<String>>) -> Self {
        Self {
            base_url: base_url.into(),
            workspace_id: workspace_id.map(Into::into),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerListRequest {
    pub runtime_id: Option<String>,
    pub include_stopped: bool,
}

impl WorkerListRequest {
    pub fn new(runtime_id: Option<String>) -> Self {
        Self {
            runtime_id,
            include_stopped: false,
        }
    }

    pub fn with_stopped(runtime_id: Option<String>) -> Self {
        Self {
            runtime_id,
            include_stopped: true,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerConnectionSelector {
    pub runtime_id: String,
    pub worker_id: String,
}

impl WorkerConnectionSelector {
    pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
        Self {
            runtime_id: runtime_id.into(),
            worker_id: worker_id.into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerSpawn {
    pub runtime_command: WorkerRuntimeCommand,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerByName {
    pub runtime_command: WorkerRuntimeCommand,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerResume {
    pub runtime_command: WorkerRuntimeCommand,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Dashboard {
    Local {
        runtime_command: WorkerRuntimeCommand,
    },
    Backend {
        base_url: String,
        workspace_id: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerList {
    pub local_runtime_command: Option<WorkerRuntimeCommand>,
    pub backend_target: Option<BackendRuntimeListTarget>,
    pub include_stopped: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerConnection {
    pub target: BackendRuntimeTarget,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetError {
    message: String,
}

impl TargetError {
    fn unsupported(operation: &'static str, target: TargetKind) -> Self {
        Self {
            message: format!("{operation} is not supported by {target} target"),
        }
    }

    fn invalid(target: TargetKind, message: impl Into<String>) -> Self {
        Self {
            message: format!("invalid {target} target: {}", message.into()),
        }
    }

    fn local_runtime_command(error: std::io::Error) -> Self {
        Self {
            message: format!("failed to resolve local Worker runtime command: {error}"),
        }
    }
}

impl fmt::Display for TargetError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for TargetError {}

pub trait Target: fmt::Debug + Send + Sync {
    fn kind(&self) -> TargetKind;

    /// Resolve the target once for Workspace product-state operations.
    ///
    /// Backend targets must carry an explicit Workspace identity. Callers use
    /// this value instead of rediscovering Backend/local authority from cwd or
    /// process configuration after command dispatch.
    fn resolve(&self) -> Result<ResolvedTarget, TargetError>;

    fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError>;

    fn worker_by_name(&self) -> Result<WorkerByName, TargetError>;

    fn resume_worker(&self) -> Result<WorkerResume, TargetError>;

    fn dashboard(&self) -> Result<Dashboard, TargetError>;

    fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError>;

    fn connect_worker(
        &self,
        selector: WorkerConnectionSelector,
    ) -> Result<WorkerConnection, TargetError>;
}

impl Target for LocalTarget {
    fn kind(&self) -> TargetKind {
        TargetKind::Local
    }

    fn resolve(&self) -> Result<ResolvedTarget, TargetError> {
        Ok(ResolvedTarget::Local)
    }

    fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
        Ok(WorkerSpawn {
            runtime_command: self.runtime_command()?,
        })
    }

    fn worker_by_name(&self) -> Result<WorkerByName, TargetError> {
        Ok(WorkerByName {
            runtime_command: self.runtime_command()?,
        })
    }

    fn resume_worker(&self) -> Result<WorkerResume, TargetError> {
        Ok(WorkerResume {
            runtime_command: self.runtime_command()?,
        })
    }

    fn dashboard(&self) -> Result<Dashboard, TargetError> {
        Ok(Dashboard::Local {
            runtime_command: self.runtime_command()?,
        })
    }

    fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
        if request.runtime_id.is_some() {
            return Err(TargetError::unsupported(
                "Explicit runtime id for local worker listing",
                self.kind(),
            ));
        }
        Ok(WorkerList {
            local_runtime_command: Some(self.runtime_command()?),
            backend_target: None,
            include_stopped: request.include_stopped,
        })
    }

    fn connect_worker(
        &self,
        _selector: WorkerConnectionSelector,
    ) -> Result<WorkerConnection, TargetError> {
        Err(TargetError::unsupported(
            "Backend runtime worker connection",
            self.kind(),
        ))
    }
}

impl Target for BackendTarget {
    fn kind(&self) -> TargetKind {
        TargetKind::Backend
    }

    fn resolve(&self) -> Result<ResolvedTarget, TargetError> {
        let workspace_id = self.workspace_id.clone().ok_or_else(|| {
            TargetError::invalid(
                self.kind(),
                "workspace selection is required for Backend product-state operations",
            )
        })?;
        Ok(ResolvedTarget::Backend {
            base_url: self.base_url.clone(),
            workspace_id,
        })
    }

    fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
        Err(TargetError::unsupported("Worker spawn", self.kind()))
    }

    fn worker_by_name(&self) -> Result<WorkerByName, TargetError> {
        Err(TargetError::unsupported(
            "Worker name attachment",
            self.kind(),
        ))
    }

    fn resume_worker(&self) -> Result<WorkerResume, TargetError> {
        Err(TargetError::unsupported("Worker resume", self.kind()))
    }

    fn dashboard(&self) -> Result<Dashboard, TargetError> {
        match self.resolve()? {
            ResolvedTarget::Backend {
                base_url,
                workspace_id,
            } => Ok(Dashboard::Backend {
                base_url,
                workspace_id,
            }),
            ResolvedTarget::Local => unreachable!("BackendTarget cannot resolve as Local"),
        }
    }

    fn list_workers(&self, request: WorkerListRequest) -> Result<WorkerList, TargetError> {
        Ok(WorkerList {
            local_runtime_command: None,
            backend_target: Some(BackendRuntimeListTarget::new(
                self.base_url.clone(),
                self.workspace_id.clone(),
                request.runtime_id,
            )),
            include_stopped: request.include_stopped,
        })
    }

    fn connect_worker(
        &self,
        selector: WorkerConnectionSelector,
    ) -> Result<WorkerConnection, TargetError> {
        let workspace_id = self.workspace_id.clone().ok_or_else(|| {
            TargetError::invalid(
                self.kind(),
                "workspace selection is required before connecting to a Backend Worker",
            )
        })?;
        Ok(WorkerConnection {
            target: BackendRuntimeTarget::new(
                self.base_url.clone(),
                workspace_id,
                selector.runtime_id,
                selector.worker_id,
            ),
        })
    }
}

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

    #[test]
    fn backend_target_resolves_workspace_scoped_product_state_authority() {
        let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));

        assert_eq!(
            target.resolve().unwrap(),
            ResolvedTarget::Backend {
                base_url: "http://127.0.0.1:8787".to_string(),
                workspace_id: "workspace-a".to_string(),
            }
        );
    }

    #[test]
    fn backend_target_rejects_product_state_resolution_without_workspace() {
        let target = BackendTarget::new("http://127.0.0.1:8787", None::<String>);

        assert!(
            target
                .resolve()
                .unwrap_err()
                .to_string()
                .contains("workspace selection is required")
        );
    }

    #[test]
    fn local_target_resolves_local_product_state_authority() {
        assert_eq!(LocalTarget::new().resolve().unwrap(), ResolvedTarget::Local);
    }

    #[test]
    fn backend_target_builds_workspace_scoped_dashboard() {
        let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));

        assert_eq!(
            target.dashboard().unwrap(),
            Dashboard::Backend {
                base_url: "http://127.0.0.1:8787".to_string(),
                workspace_id: "workspace-a".to_string(),
            }
        );
    }

    #[test]
    fn backend_target_rejects_dashboard_without_workspace_selection() {
        let target = BackendTarget::new("http://127.0.0.1:8787", None::<String>);

        assert!(
            target
                .dashboard()
                .unwrap_err()
                .to_string()
                .contains("workspace selection is required")
        );
    }

    #[test]
    fn backend_target_builds_worker_list() {
        let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));
        let workers = target
            .list_workers(WorkerListRequest::new(Some("runtime-a".to_string())))
            .unwrap();

        assert_eq!(
            workers.backend_target.as_ref().unwrap().base_url,
            "http://127.0.0.1:8787"
        );
        assert_eq!(
            workers
                .backend_target
                .as_ref()
                .unwrap()
                .workspace_id
                .as_deref(),
            Some("workspace-a")
        );
        assert_eq!(
            workers
                .backend_target
                .as_ref()
                .unwrap()
                .runtime_id
                .as_deref(),
            Some("runtime-a")
        );
    }

    #[test]
    fn backend_target_builds_worker_connection() {
        let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));
        let connection = target
            .connect_worker(WorkerConnectionSelector::new("runtime-a", "worker-b"))
            .unwrap();

        assert_eq!(connection.target.base_url, "http://127.0.0.1:8787");
        assert_eq!(connection.target.workspace_id, "workspace-a");
        assert_eq!(connection.target.runtime_id, "runtime-a");
        assert_eq!(connection.target.worker_id, "worker-b");
    }

    #[test]
    fn backend_target_rejects_worker_connection_before_workspace_selection() {
        let target = BackendTarget::new("http://127.0.0.1:8787", None::<String>);
        let error =
            match target.connect_worker(WorkerConnectionSelector::new("runtime-a", "worker-b")) {
                Ok(_) => panic!("unscoped connection must fail"),
                Err(error) => error,
            };

        assert!(
            error
                .to_string()
                .contains("workspace selection is required")
        );
    }

    #[test]
    fn backend_target_rejects_local_worker_operations() {
        let target = BackendTarget::new("http://127.0.0.1:8787", None::<String>);
        let err = target.spawn_worker().unwrap_err();

        assert_eq!(
            err.to_string(),
            "Worker spawn is not supported by Backend target"
        );
    }

    #[test]
    fn local_target_builds_local_worker_list() {
        let target = LocalTarget::new();
        let workers = target
            .list_workers(WorkerListRequest::with_stopped(None))
            .unwrap();

        assert!(workers.local_runtime_command.is_some());
        assert!(workers.backend_target.is_none());
        assert!(workers.include_stopped);
    }
}
