//! `mission_client` — REST client to the external `missions` API.
//!
//! Public surface (per `module-layout.md`): [`MissionClient`],
//! [`MissionClientHandle`], the typed [`Mission`] DTO, [`FetchError`], and
//! [`MissionClientOptions`].
//!
//! Real implementation tasks: AZ-644 (pull + schema, this file), AZ-645
//! (middle-waypoint POST), AZ-646 (mapobjects pull), AZ-647 (mapobjects push).

mod internal;

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;

use serde::{Deserialize, Serialize};
use shared::error::{AutopilotError, Result};
use shared::health::ComponentHealth;
use shared::models::mapobject::MapObjectsBundle;
use shared::models::mission::{Coordinate, Geofence, MissionItem};
use uuid::Uuid;

use internal::missions_api::HttpClient;

const NAME: &str = "mission_client";

/// Mission DTO returned by `pull_mission`. Shape matches the JSON wire schema
/// in `shared/contracts/mission-schema.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Mission {
    pub mission_id: Uuid,
    pub schema_version: String,
    pub items: Vec<MissionItem>,
    pub geofences: Vec<Geofence>,
    pub return_point: Coordinate,
}

/// Errors surfaced by `MissionClientHandle::pull_mission`.
#[derive(Debug, thiserror::Error)]
pub enum FetchError {
    /// JSON body did not match the bundled `mission-schema`. Includes a
    /// size-capped sample of the raw body for offline analysis.
    #[error("mission schema invalid: {}", messages.join("; "))]
    SchemaInvalid {
        messages: Vec<String>,
        sample: String,
    },
    /// Non-retryable HTTP or transport-level error (4xx, malformed URL, etc.).
    #[error("permanent fetch failure: {0}")]
    Permanent(String),
    /// Retried up to `max_attempts` without success.
    #[error("max retries exceeded after {attempts} attempts")]
    MaxRetriesExceeded { attempts: u32 },
    /// Local bug (deserialisation after schema validation succeeded, etc.).
    #[error("internal error: {0}")]
    Internal(String),
}

impl From<FetchError> for AutopilotError {
    fn from(e: FetchError) -> Self {
        match e {
            FetchError::SchemaInvalid { messages, .. } => {
                AutopilotError::Validation(messages.join("; "))
            }
            FetchError::Permanent(s) => AutopilotError::Network(s),
            FetchError::MaxRetriesExceeded { attempts } => {
                AutopilotError::Network(format!("max retries exceeded after {attempts} attempts"))
            }
            FetchError::Internal(s) => AutopilotError::Internal(s),
        }
    }
}

/// Tunables for the missions-API client. AZ-644 §NFR defaults: 5 attempts,
/// 200 ms base / 5 s cap, 5 s startup-fetch budget.
#[derive(Debug, Clone)]
pub struct MissionClientOptions {
    pub endpoint: String,
    pub bearer_token: Option<String>,
    pub max_attempts: u32,
    pub backoff_base: Duration,
    pub backoff_cap: Duration,
    pub request_timeout: Duration,
    pub connect_timeout: Duration,
}

impl MissionClientOptions {
    pub fn new(endpoint: impl Into<String>) -> Self {
        Self {
            endpoint: endpoint.into(),
            bearer_token: None,
            max_attempts: 5,
            backoff_base: Duration::from_millis(200),
            backoff_cap: Duration::from_secs(5),
            request_timeout: Duration::from_secs(5),
            connect_timeout: Duration::from_secs(2),
        }
    }
}

#[derive(Debug, Default)]
struct ClientState {
    last_fetch_unix_s: AtomicU64,
    fetch_errors_total: AtomicU64,
    last_schema_version: std::sync::Mutex<Option<String>>,
    last_connection_state: std::sync::Mutex<ConnectionState>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum ConnectionState {
    #[default]
    Unknown,
    Ok,
    Error,
}

impl ConnectionState {
    fn label(&self) -> &'static str {
        match self {
            Self::Unknown => "unknown",
            Self::Ok => "ok",
            Self::Error => "error",
        }
    }
}

/// Public client. Build once at startup and pass the [`MissionClientHandle`]
/// to other components.
#[derive(Debug)]
pub struct MissionClient {
    options: MissionClientOptions,
    http: HttpClient,
    state: Arc<ClientState>,
}

impl MissionClient {
    pub fn new(options: MissionClientOptions) -> std::result::Result<Self, FetchError> {
        let http = HttpClient::new(&options)?;
        Ok(Self {
            options,
            http,
            state: Arc::new(ClientState::default()),
        })
    }

    pub fn handle(&self) -> MissionClientHandle {
        MissionClientHandle {
            options: self.options.clone(),
            http: self.http.clone(),
            state: self.state.clone(),
        }
    }
}

/// Clonable handle. Each clone shares the same HTTP client and counters.
#[derive(Debug, Clone)]
pub struct MissionClientHandle {
    options: MissionClientOptions,
    http: HttpClient,
    state: Arc<ClientState>,
}

impl MissionClientHandle {
    /// Fetch + validate a mission by id. Implements bounded exponential
    /// backoff and rejects schema-invalid responses without a silent downcast.
    pub async fn pull_mission(&self, mission_id: &str) -> std::result::Result<Mission, FetchError> {
        match self.http.pull_mission_raw(mission_id, &self.options).await {
            Ok(value) => {
                let mission: Mission = serde_json::from_value(value)
                    .map_err(|e| FetchError::Internal(format!("deserialise mission: {e}")))?;
                self.state
                    .last_fetch_unix_s
                    .store(now_unix_s(), Ordering::Relaxed);
                *self.state.last_schema_version.lock().unwrap() =
                    Some(mission.schema_version.clone());
                *self.state.last_connection_state.lock().unwrap() = ConnectionState::Ok;
                Ok(mission)
            }
            Err(e) => {
                self.state
                    .fetch_errors_total
                    .fetch_add(1, Ordering::Relaxed);
                *self.state.last_connection_state.lock().unwrap() = ConnectionState::Error;
                Err(e)
            }
        }
    }

    pub async fn post_middle_waypoint(&self, _mission_id: &str, _at: Coordinate) -> Result<()> {
        Err(AutopilotError::NotImplemented(
            "mission_client::post_middle_waypoint (AZ-645)",
        ))
    }

    pub async fn pull_mapobjects(&self, _mission_id: &str) -> Result<MapObjectsBundle> {
        Err(AutopilotError::NotImplemented(
            "mission_client::pull_mapobjects (AZ-646)",
        ))
    }

    pub async fn push_mapobjects(&self, _bundle: MapObjectsBundle) -> Result<()> {
        Err(AutopilotError::NotImplemented(
            "mission_client::push_mapobjects (AZ-647)",
        ))
    }

    pub fn health(&self) -> ComponentHealth {
        let conn = *self.state.last_connection_state.lock().unwrap();
        let last_fetch = self.state.last_fetch_unix_s.load(Ordering::Relaxed);
        let errors = self.state.fetch_errors_total.load(Ordering::Relaxed);
        let schema_version = self
            .state
            .last_schema_version
            .lock()
            .unwrap()
            .clone()
            .unwrap_or_else(|| "none".to_owned());
        let detail = format!(
            "last_fetch_ts={} fetch_errors_total={} schema_version={} connection_state={}",
            if last_fetch == 0 {
                "none".into()
            } else {
                last_fetch.to_string()
            },
            errors,
            schema_version,
            conn.label(),
        );
        match conn {
            ConnectionState::Ok => ComponentHealth::green(NAME),
            ConnectionState::Error => ComponentHealth::red(NAME, detail),
            ConnectionState::Unknown => ComponentHealth::yellow(NAME, detail),
        }
    }
}

fn now_unix_s() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

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

    #[test]
    fn fetch_error_maps_to_autopilot_error() {
        let e: AutopilotError = FetchError::Permanent("boom".into()).into();
        match e {
            AutopilotError::Network(s) => assert!(s.contains("boom")),
            other => panic!("expected Network, got {other:?}"),
        }
    }
}
