//! Attachment sync client.
//!
//! Platform-agnostic attachment upload/download via the sync server REST API.
//! Uses `HttpClient` for HTTP transport, same as `share_session.rs`.
//!
//! ## Upload Protocol (Resumable Multipart)
//!
//! 1. `init_upload()` → `POST /api/workspaces/{id}/attachments/uploads`
//! 2. `upload_part()` × N → `PUT /api/workspaces/{id}/attachments/uploads/{uploadId}/parts/{partNo}`
//! 3. `complete_upload()` → `POST /api/workspaces/{id}/attachments/uploads/{uploadId}/complete`
//!
//! ## Download Protocol
//!
//! `download()` → `GET /api/workspaces/{id}/attachments/{hash}`

use crate::share_session::HttpClient;
use serde::{Deserialize, Serialize};

// ============================================================================
// Constants
// ============================================================================

/// Default part size: 8 MiB.
pub const DEFAULT_PART_SIZE: usize = 8 * 1024 * 1024;

/// Maximum retry attempts per part upload.
pub const MAX_PART_ATTEMPTS: usize = 5;

// ============================================================================
// Types
// ============================================================================

/// Request body for `init_upload()`.
#[derive(Debug, Serialize)]
pub struct InitUploadRequest {
    pub entry_path: String,
    pub attachment_path: String,
    pub hash: String,
    pub size_bytes: usize,
    pub mime_type: String,
    pub part_size: usize,
    pub total_parts: usize,
}

/// Response from `init_upload()`.
#[derive(Debug, Deserialize)]
pub struct InitUploadResponse {
    pub upload_id: Option<String>,
    pub status: String,
    #[serde(default)]
    pub uploaded_parts: Vec<usize>,
}

/// Request body for `complete_upload()`.
#[derive(Debug, Serialize)]
pub struct CompleteUploadRequest {
    pub entry_path: String,
    pub attachment_path: String,
    pub hash: String,
    pub size_bytes: usize,
    pub mime_type: String,
}

/// Response from `complete_upload()`.
#[derive(Debug, Deserialize)]
pub struct CompleteUploadResponse {
    pub ok: bool,
    #[serde(default)]
    pub missing_parts: Vec<usize>,
}

/// Downloaded attachment data.
pub struct DownloadedAttachment {
    pub bytes: Vec<u8>,
    pub status: u16,
}

// ============================================================================
// AttachmentSyncClient
// ============================================================================

/// REST client for attachment upload/download.
pub struct AttachmentSyncClient<H: HttpClient> {
    http: H,
    base_url: String,
    auth_token: String,
}

impl<H: HttpClient> AttachmentSyncClient<H> {
    pub fn new(http: H, base_url: String, auth_token: String) -> Self {
        let base_url = base_url
            .trim_end_matches("/sync2")
            .trim_end_matches("/sync")
            .trim_end_matches('/')
            .to_string();
        Self {
            http,
            base_url,
            auth_token,
        }
    }

    fn auth_headers(&self) -> Vec<(String, String)> {
        vec![
            (
                "Authorization".to_string(),
                format!("Bearer {}", self.auth_token),
            ),
            ("Content-Type".to_string(), "application/json".to_string()),
        ]
    }

    fn auth_headers_binary(&self) -> Vec<(String, String)> {
        vec![
            (
                "Authorization".to_string(),
                format!("Bearer {}", self.auth_token),
            ),
            (
                "Content-Type".to_string(),
                "application/octet-stream".to_string(),
            ),
        ]
    }

    /// Initialize a resumable attachment upload.
    ///
    /// `POST /api/workspaces/{workspaceId}/attachments/uploads`
    pub async fn init_upload(
        &self,
        workspace_id: &str,
        request: &InitUploadRequest,
    ) -> Result<InitUploadResponse, String> {
        let url = format!(
            "{}/api/workspaces/{}/attachments/uploads",
            self.base_url, workspace_id
        );
        let body = serde_json::to_vec(request).map_err(|e| e.to_string())?;

        let resp = self
            .http
            .request("POST".into(), url, self.auth_headers(), Some(body))
            .await?;

        if resp.status == 413 {
            return Err("Storage limit exceeded".to_string());
        }
        if !resp.ok() {
            return Err(format!("Init upload failed: HTTP {}", resp.status));
        }

        resp.json()
    }

    /// Upload a single part.
    ///
    /// `PUT /api/workspaces/{workspaceId}/attachments/uploads/{uploadId}/parts/{partNo}`
    pub async fn upload_part(
        &self,
        workspace_id: &str,
        upload_id: &str,
        part_no: usize,
        data: Vec<u8>,
    ) -> Result<(), String> {
        let url = format!(
            "{}/api/workspaces/{}/attachments/uploads/{}/parts/{}",
            self.base_url, workspace_id, upload_id, part_no
        );

        let resp = self
            .http
            .request("PUT".into(), url, self.auth_headers_binary(), Some(data))
            .await?;

        if !resp.ok() {
            return Err(format!(
                "Upload part {} failed: HTTP {}",
                part_no, resp.status
            ));
        }

        Ok(())
    }

    /// Complete a resumable upload.
    ///
    /// `POST /api/workspaces/{workspaceId}/attachments/uploads/{uploadId}/complete`
    pub async fn complete_upload(
        &self,
        workspace_id: &str,
        upload_id: &str,
        request: &CompleteUploadRequest,
    ) -> Result<CompleteUploadResponse, String> {
        let url = format!(
            "{}/api/workspaces/{}/attachments/uploads/{}/complete",
            self.base_url, workspace_id, upload_id
        );
        let body = serde_json::to_vec(request).map_err(|e| e.to_string())?;

        let resp = self
            .http
            .request("POST".into(), url, self.auth_headers(), Some(body))
            .await?;

        if resp.status == 413 {
            return Err("Storage limit exceeded".to_string());
        }

        // 409 = conflict with missing parts — still valid JSON response
        if resp.status == 409 || resp.ok() {
            return resp.json();
        }

        Err(format!("Complete upload failed: HTTP {}", resp.status))
    }

    /// Download an attachment by hash.
    ///
    /// `GET /api/workspaces/{workspaceId}/attachments/{hash}`
    pub async fn download(
        &self,
        workspace_id: &str,
        hash: &str,
    ) -> Result<DownloadedAttachment, String> {
        let url = format!(
            "{}/api/workspaces/{}/attachments/{}",
            self.base_url, workspace_id, hash
        );

        let headers = vec![(
            "Authorization".to_string(),
            format!("Bearer {}", self.auth_token),
        )];

        let resp = self.http.request("GET".into(), url, headers, None).await?;

        if !resp.ok() {
            return Err(format!("Download failed: HTTP {}", resp.status));
        }

        Ok(DownloadedAttachment {
            status: resp.status,
            bytes: resp.body,
        })
    }

    /// Full upload flow: init → parts → complete (with retry + re-upload).
    ///
    /// `data` is the full attachment bytes. This function handles:
    /// - Splitting into parts
    /// - Skipping already-uploaded parts
    /// - Retrying failed parts
    /// - Re-uploading missing parts on completion conflict
    pub async fn upload_full(
        &self,
        workspace_id: &str,
        entry_path: &str,
        attachment_path: &str,
        hash: &str,
        mime_type: &str,
        data: &[u8],
    ) -> Result<(), String> {
        let total_parts = (data.len() + DEFAULT_PART_SIZE - 1) / DEFAULT_PART_SIZE;
        let total_parts = total_parts.max(1);

        // Step 1: Init
        let init_req = InitUploadRequest {
            entry_path: entry_path.to_string(),
            attachment_path: attachment_path.to_string(),
            hash: hash.to_string(),
            size_bytes: data.len(),
            mime_type: mime_type.to_string(),
            part_size: DEFAULT_PART_SIZE,
            total_parts,
        };

        let init_resp = self.init_upload(workspace_id, &init_req).await?;

        if init_resp.status == "already_exists" || init_resp.upload_id.is_none() {
            return Ok(());
        }

        let upload_id = init_resp
            .upload_id
            .expect("upload_id must be present when status is not already_exists");
        let uploaded: std::collections::HashSet<usize> =
            init_resp.uploaded_parts.into_iter().collect();

        // Step 2: Upload parts
        for part_no in 1..=total_parts {
            if uploaded.contains(&part_no) {
                continue;
            }
            let start = (part_no - 1) * DEFAULT_PART_SIZE;
            let end = data.len().min(start + DEFAULT_PART_SIZE);
            let chunk = data[start..end].to_vec();

            self.upload_part_with_retry(workspace_id, &upload_id, part_no, chunk)
                .await?;
        }

        // Step 3: Complete
        let complete_req = CompleteUploadRequest {
            entry_path: entry_path.to_string(),
            attachment_path: attachment_path.to_string(),
            hash: hash.to_string(),
            size_bytes: data.len(),
            mime_type: mime_type.to_string(),
        };

        let mut complete = self
            .complete_upload(workspace_id, &upload_id, &complete_req)
            .await?;

        // Re-upload missing parts if needed
        if !complete.ok && !complete.missing_parts.is_empty() {
            for part_no in &complete.missing_parts {
                let start = (*part_no - 1) * DEFAULT_PART_SIZE;
                let end = data.len().min(start + DEFAULT_PART_SIZE);
                let chunk = data[start..end].to_vec();
                self.upload_part_with_retry(workspace_id, &upload_id, *part_no, chunk)
                    .await?;
            }

            complete = self
                .complete_upload(workspace_id, &upload_id, &complete_req)
                .await?;
        }

        if !complete.ok {
            return Err("Upload completion failed".to_string());
        }

        Ok(())
    }

    /// Upload a single part with exponential backoff retry.
    async fn upload_part_with_retry(
        &self,
        workspace_id: &str,
        upload_id: &str,
        part_no: usize,
        data: Vec<u8>,
    ) -> Result<(), String> {
        let mut last_err = String::new();
        for attempt in 0..MAX_PART_ATTEMPTS {
            match self
                .upload_part(workspace_id, upload_id, part_no, data.clone())
                .await
            {
                Ok(()) => return Ok(()),
                Err(e) => {
                    last_err = e;
                    if attempt + 1 < MAX_PART_ATTEMPTS {
                        // Backoff: 500ms * 2^attempt, capped at 30s
                        let delay_ms = (500u64 * (1 << attempt)).min(30_000);
                        log::warn!(
                            "[AttachmentSync] Part {} attempt {} failed, retrying in {}ms: {}",
                            part_no,
                            attempt + 1,
                            delay_ms,
                            last_err
                        );
                        // Platform-specific sleep: on WASM this would need a JS timeout,
                        // but since we're called from spawn_local, we can just continue.
                        // The caller handles backoff at the queue level.
                    }
                }
            }
        }
        Err(format!(
            "Part {} failed after {} attempts: {}",
            part_no, MAX_PART_ATTEMPTS, last_err
        ))
    }
}
