use std::sync::Arc;
use std::time::Duration;

use forage_core::integrations::router::{DispatchTask, NotificationEvent, ReleaseContext};
use forage_core::integrations::webhook::sign_payload;
use forage_core::integrations::{DeliveryStatus, IntegrationStore};
use notmad::{Component, ComponentInfo, MadError};
use tokio_util::sync::CancellationToken;

use crate::forest_client::GrpcForestClient;

// ── Dispatcher ──────────────────────────────────────────────────────

/// HTTP client for dispatching webhooks and Slack messages.
pub struct NotificationDispatcher {
    http: reqwest::Client,
    store: Arc<dyn IntegrationStore>,
}

impl NotificationDispatcher {
    pub fn new(store: Arc<dyn IntegrationStore>) -> Self {
        let http = reqwest::Client::builder()
            .timeout(Duration::from_secs(10))
            .build()
            .expect("failed to build reqwest client");
        Self { http, store }
    }

    /// Execute a dispatch task with retry (3 attempts, exponential backoff).
    pub async fn dispatch(&self, task: &DispatchTask) {
        let (integration_id, notification_id) = match task {
            DispatchTask::Webhook {
                integration_id,
                payload,
                ..
            } => (integration_id.clone(), payload.notification_id.clone()),
            DispatchTask::Slack {
                integration_id, ..
            } => (integration_id.clone(), String::new()),
        };

        let delays = [1, 5, 25]; // seconds
        for (attempt, delay) in delays.iter().enumerate() {
            match self.try_dispatch(task).await {
                Ok(()) => {
                    tracing::info!(
                        integration_id = %integration_id,
                        attempt = attempt + 1,
                        "notification delivered"
                    );
                    let _ = self
                        .store
                        .record_delivery(&integration_id, &notification_id, DeliveryStatus::Delivered, None)
                        .await;
                    return;
                }
                Err(e) => {
                    tracing::warn!(
                        integration_id = %integration_id,
                        attempt = attempt + 1,
                        error = %e,
                        "delivery attempt failed"
                    );
                    if attempt < delays.len() - 1 {
                        tokio::time::sleep(Duration::from_secs(*delay)).await;
                    } else {
                        tracing::error!(
                            integration_id = %integration_id,
                            "all delivery attempts exhausted"
                        );
                        let _ = self
                            .store
                            .record_delivery(
                                &integration_id,
                                &notification_id,
                                DeliveryStatus::Failed,
                                Some(&e),
                            )
                            .await;
                    }
                }
            }
        }
    }

    async fn try_dispatch(&self, task: &DispatchTask) -> Result<(), String> {
        match task {
            DispatchTask::Webhook {
                url,
                secret,
                headers,
                payload,
                ..
            } => {
                let body =
                    serde_json::to_vec(payload).map_err(|e| format!("serialize: {e}"))?;

                let mut req = self
                    .http
                    .post(url)
                    .header("Content-Type", "application/json")
                    .header("User-Agent", "Forage/1.0");

                if let Some(secret) = secret {
                    let sig = sign_payload(&body, secret);
                    req = req.header("X-Forage-Signature", sig);
                }

                for (k, v) in headers {
                    req = req.header(k.as_str(), v.as_str());
                }

                let resp = req
                    .body(body)
                    .send()
                    .await
                    .map_err(|e| format!("http: {e}"))?;

                let status = resp.status();
                if status.is_success() {
                    Ok(())
                } else {
                    let body = resp.text().await.unwrap_or_default();
                    Err(format!("HTTP {status}: {body}"))
                }
            }
            DispatchTask::Slack {
                webhook_url,
                message,
                ..
            } => {
                // Use Block Kit attachments for rich formatting
                let payload = serde_json::json!({
                    "text": message.text,
                    "attachments": [{
                        "color": message.color,
                        "blocks": message.blocks,
                    }]
                });

                let resp = self
                    .http
                    .post(webhook_url)
                    .header("Content-Type", "application/json")
                    .json(&payload)
                    .send()
                    .await
                    .map_err(|e| format!("slack http: {e}"))?;

                let status = resp.status();
                if status.is_success() {
                    Ok(())
                } else {
                    let body = resp.text().await.unwrap_or_default();
                    Err(format!("Slack HTTP {status}: {body}"))
                }
            }
        }
    }
}

// ── Proto conversion ────────────────────────────────────────────────

/// Convert a proto Notification to our domain NotificationEvent.
pub fn proto_to_event(n: &forage_grpc::Notification) -> NotificationEvent {
    let notification_type = match n.notification_type() {
        forage_grpc::NotificationType::ReleaseAnnotated => "release_annotated",
        forage_grpc::NotificationType::ReleaseStarted => "release_started",
        forage_grpc::NotificationType::ReleaseSucceeded => "release_succeeded",
        forage_grpc::NotificationType::ReleaseFailed => "release_failed",
        _ => "unknown",
    };

    let release = n.release_context.as_ref().map(|r| ReleaseContext {
        slug: r.slug.clone(),
        artifact_id: r.artifact_id.clone(),
        destination: r.destination.clone(),
        environment: r.environment.clone(),
        source_username: r.source_username.clone(),
        commit_sha: r.commit_sha.clone(),
        commit_branch: r.commit_branch.clone(),
        error_message: if r.error_message.is_empty() {
            None
        } else {
            Some(r.error_message.clone())
        },
    });

    NotificationEvent {
        id: n.id.clone(),
        notification_type: notification_type.to_string(),
        title: n.title.clone(),
        body: n.body.clone(),
        organisation: n.organisation.clone(),
        project: n.project.clone(),
        timestamp: n.created_at.clone(),
        release,
    }
}

// ── Listener component ──────────────────────────────────────────────

/// Background component that listens to Forest's notification stream
/// for all orgs with active integrations, and dispatches to configured channels.
pub struct NotificationListener {
    pub grpc: Arc<GrpcForestClient>,
    pub store: Arc<dyn IntegrationStore>,
    /// Service token (PAT) for authenticating with forest-server's NotificationService.
    pub service_token: String,
}

impl Component for NotificationListener {
    fn info(&self) -> ComponentInfo {
        "forage/notification-listener".into()
    }

    async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
        let dispatcher = Arc::new(NotificationDispatcher::new(self.store.clone()));

        // For now, listen on the global stream (no org filter).
        // Forest's ListenNotifications with no org filter returns all notifications
        // the authenticated user has access to.
        let mut backoff = 1u64;

        loop {
            tokio::select! {
                _ = cancellation_token.cancelled() => {
                    tracing::info!("notification listener shutting down");
                    break;
                }
                result = self.listen_once(&dispatcher) => {
                    match result {
                        Ok(()) => {
                            tracing::info!("notification stream ended cleanly");
                            backoff = 1;
                        }
                        Err(e) => {
                            tracing::error!(error = %e, backoff_secs = backoff, "notification stream error, reconnecting");
                        }
                    }

                    // Wait before reconnecting, but respect cancellation
                    tokio::select! {
                        _ = cancellation_token.cancelled() => break,
                        _ = tokio::time::sleep(Duration::from_secs(backoff)) => {}
                    }
                    backoff = (backoff * 2).min(60);
                }
            }
        }

        Ok(())
    }
}

impl NotificationListener {
    async fn listen_once(&self, dispatcher: &Arc<NotificationDispatcher>) -> Result<(), String> {
        use futures_util::StreamExt;

        let mut client = self.grpc.notification_client();

        let mut req = tonic::Request::new(forage_grpc::ListenNotificationsRequest {
            organisation: None,
            project: None,
        });
        req.metadata_mut().insert(
            "authorization",
            format!("Bearer {}", self.service_token)
                .parse()
                .map_err(|e| format!("invalid service token: {e}"))?,
        );

        let response = client
            .listen_notifications(req)
            .await
            .map_err(|e| format!("gRPC connect: {e}"))?;

        let mut stream = response.into_inner();

        tracing::info!("connected to notification stream");

        while let Some(result) = stream.next().await {
            match result {
                Ok(notification) => {
                    let event = proto_to_event(&notification);
                    tracing::info!(
                        org = %event.organisation,
                        event_type = %event.notification_type,
                        notification_id = %event.id,
                        "received notification"
                    );

                    let tasks = forage_core::integrations::router::route_notification_for_org(
                        self.store.as_ref(),
                        &event,
                    )
                    .await;

                    for task in &tasks {
                        let dispatcher = dispatcher.clone();
                        let task = task.clone();
                        tokio::spawn(async move {
                            dispatcher.dispatch(&task).await;
                        });
                    }
                }
                Err(e) => {
                    return Err(format!("stream error: {e}"));
                }
            }
        }

        Ok(())
    }
}
