//! Export data as ZIP archives with auto-chunking at 4GB.
//!
//! Export scopes:
//! - IdentityOnly: just identity.key (tiny backup)
//! - PostsOnly: public posts + blobs (no key — safe to share)
//! - PostsWithIdentity: posts + blobs + identity.key (full migration)
//! - Everything: posts + blobs + key + follows + profiles + settings (complete backup)

use std::io::Write;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use tracing::info;

use crate::blob::BlobStore;
use crate::storage::StoragePool;
use crate::types::NodeId;

/// Maximum bytes per ZIP chunk (4 GB).
const CHUNK_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;

/// What to include in the export.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExportScope {
    IdentityOnly,
    PostsOnly,
    PostsWithIdentity,
    Everything,
}

/// Manifest embedded in each ZIP chunk.
#[derive(Debug, Serialize, Deserialize)]
pub struct ExportManifest {
    pub version: u32,
    pub scope: ExportScope,
    pub node_id: String,
    pub export_date: u64,
    pub chunk_index: u32,
    pub total_chunks: u32,
    pub post_count: usize,
    pub blob_count: usize,
}

/// Exported post with visibility and header.
#[derive(Debug, Serialize, Deserialize)]
pub struct ExportedPost {
    pub id: String,
    pub author: String,
    pub content: String,
    pub attachments_json: String,
    pub timestamp_ms: u64,
    pub visibility_json: String,
    pub header_json: Option<String>,
    pub intent: Option<String>,
}

/// Result of an export operation.
pub struct ExportResult {
    pub paths: Vec<PathBuf>,
    pub post_count: usize,
    pub blob_count: usize,
}

/// Run an export to the given output directory.
pub async fn export_data(
    data_dir: &Path,
    storage: &StoragePool,
    blob_store: &BlobStore,
    node_id: &NodeId,
    scope: ExportScope,
    output_dir: &Path,
) -> anyhow::Result<ExportResult> {
    std::fs::create_dir_all(output_dir)?;

    let node_id_hex = hex::encode(node_id);
    let now = now_ms();

    // Gather data based on scope
    let identity_key = if scope != ExportScope::PostsOnly {
        let key_path = data_dir.join("identity.key");
        if key_path.exists() {
            Some(std::fs::read(&key_path)?)
        } else {
            None
        }
    } else {
        None
    };

    let (posts, blob_cids) = if scope == ExportScope::IdentityOnly {
        (vec![], vec![])
    } else {
        gather_posts(storage, node_id).await?
    };

    let (follows, profiles, settings) = if scope == ExportScope::Everything {
        gather_extras(storage).await?
    } else {
        (None, None, None)
    };

    let post_count = posts.len();

    // Build the ZIP
    let mut zip_paths: Vec<PathBuf> = Vec::new();
    let mut chunk_index: u32 = 0;
    let mut current_size: u64 = 0;
    let base_name = format!("itsgoin-export-{}", &node_id_hex[..8]);

    let chunk_path = |idx: u32| -> PathBuf {
        if idx == 0 {
            output_dir.join(format!("{}.zip", base_name))
        } else {
            output_dir.join(format!("{}.part{}.zip", base_name, idx + 1))
        }
    };

    let path = chunk_path(chunk_index);
    let file = std::fs::File::create(&path)?;
    let mut zip = zip::ZipWriter::new(file);
    let options = zip::write::SimpleFileOptions::default()
        .compression_method(zip::CompressionMethod::Deflated);

    // Write manifest (placeholder — updated at end)
    let manifest = ExportManifest {
        version: 1,
        scope,
        node_id: node_id_hex.clone(),
        export_date: now,
        chunk_index: 0,
        total_chunks: 1, // updated later if chunked
        post_count,
        blob_count: blob_cids.len(),
    };
    let manifest_json = serde_json::to_string_pretty(&manifest)?;
    zip.start_file("itsgoin-export/manifest.json", options)?;
    zip.write_all(manifest_json.as_bytes())?;
    current_size += manifest_json.len() as u64;

    // Identity key (network key)
    if let Some(ref key) = identity_key {
        zip.start_file("itsgoin-export/identity.key", options)?;
        zip.write_all(key)?;
        current_size += key.len() as u64;
    }

    // Posting identities (0.6.3+) — list of all posting keys held by this
    // device. For upgraders the only entry is a copy of the network key;
    // for multi-persona users (0.6.4+) this is the source of truth.
    if scope != ExportScope::PostsOnly {
        let posting_identities = {
            let s = storage.get().await;
            s.list_posting_identities().unwrap_or_default()
        };
        if !posting_identities.is_empty() {
            let pi_json = serde_json::to_string_pretty(&posting_identities)?;
            zip.start_file("itsgoin-export/posting_identities.json", options)?;
            zip.write_all(pi_json.as_bytes())?;
            current_size += pi_json.len() as u64;
        }
    }

    // Posts
    if !posts.is_empty() {
        let posts_json = serde_json::to_string_pretty(&posts)?;
        zip.start_file("itsgoin-export/posts.json", options)?;
        zip.write_all(posts_json.as_bytes())?;
        current_size += posts_json.len() as u64;
    }

    // Follows, profiles, settings
    if let Some(ref data) = follows {
        let json = serde_json::to_string_pretty(data)?;
        zip.start_file("itsgoin-export/follows.json", options)?;
        zip.write_all(json.as_bytes())?;
        current_size += json.len() as u64;
    }
    if let Some(ref data) = profiles {
        let json = serde_json::to_string_pretty(data)?;
        zip.start_file("itsgoin-export/profiles.json", options)?;
        zip.write_all(json.as_bytes())?;
        current_size += json.len() as u64;
    }
    if let Some(ref data) = settings {
        let json = serde_json::to_string_pretty(data)?;
        zip.start_file("itsgoin-export/settings.json", options)?;
        zip.write_all(json.as_bytes())?;
        current_size += json.len() as u64;
    }

    // Blobs
    let mut blob_count = 0usize;
    for cid in &blob_cids {
        if let Some(data) = blob_store.get(cid)? {
            // Check if we need to start a new chunk
            if current_size + data.len() as u64 > CHUNK_MAX_BYTES && blob_count > 0 {
                zip.finish()?;
                zip_paths.push(path.clone());
                chunk_index += 1;
                let new_path = chunk_path(chunk_index);
                let new_file = std::fs::File::create(&new_path)?;
                zip = zip::ZipWriter::new(new_file);
                current_size = 0;
                // Continuation manifest
                let cont_manifest = ExportManifest {
                    version: 1,
                    scope,
                    node_id: node_id_hex.clone(),
                    export_date: now,
                    chunk_index,
                    total_chunks: 0, // unknown yet
                    post_count: 0,
                    blob_count: 0,
                };
                let cont_json = serde_json::to_string_pretty(&cont_manifest)?;
                zip.start_file("itsgoin-export/manifest.json", options)?;
                zip.write_all(cont_json.as_bytes())?;
                current_size += cont_json.len() as u64;
            }

            let cid_hex = hex::encode(cid);
            zip.start_file(format!("itsgoin-export/blobs/{}", cid_hex), options)?;
            zip.write_all(&data)?;
            current_size += data.len() as u64;
            blob_count += 1;
        }
    }

    zip.finish()?;
    zip_paths.push(chunk_path(chunk_index));

    info!(
        posts = post_count,
        blobs = blob_count,
        chunks = zip_paths.len(),
        scope = ?scope,
        "Export complete"
    );

    Ok(ExportResult {
        paths: zip_paths,
        post_count,
        blob_count,
    })
}

/// Gather own posts and their blob CIDs.
async fn gather_posts(
    storage: &StoragePool,
    node_id: &NodeId,
) -> anyhow::Result<(Vec<ExportedPost>, Vec<[u8; 32]>)> {
    let s = storage.get().await;
    let posts_with_vis = s.list_posts_with_visibility()?;
    let mut exported = Vec::new();
    let mut blob_cids = Vec::new();

    for (id, post, vis) in &posts_with_vis {
        // Only export our own posts
        if post.author != *node_id {
            continue;
        }

        let header = s.get_blob_header(id).ok().flatten().map(|(json, _)| json);
        let intent = s.get_post_intent(id).ok().flatten().map(|i| format!("{:?}", i));

        exported.push(ExportedPost {
            id: hex::encode(id),
            author: hex::encode(post.author),
            content: post.content.clone(),
            attachments_json: serde_json::to_string(&post.attachments).unwrap_or_default(),
            timestamp_ms: post.timestamp_ms,
            visibility_json: serde_json::to_string(vis).unwrap_or_default(),
            header_json: header,
            intent,
        });

        // Collect blob CIDs from attachments
        for att in &post.attachments {
            if !blob_cids.contains(&att.cid) {
                blob_cids.push(att.cid);
            }
        }
    }

    Ok((exported, blob_cids))
}

/// Gather follows, profiles, and settings for "Everything" export.
async fn gather_extras(
    storage: &StoragePool,
) -> anyhow::Result<(
    Option<Vec<String>>,           // follows (hex node_ids)
    Option<Vec<serde_json::Value>>, // profiles
    Option<Vec<(String, String)>>,  // settings (key, value)
)> {
    let s = storage.get().await;

    // Follows
    let follows: Vec<String> = s.list_follows()
        .unwrap_or_default()
        .into_iter()
        .map(|nid| hex::encode(nid))
        .collect();

    // Profiles
    let profiles: Vec<serde_json::Value> = s.list_profiles()
        .unwrap_or_default()
        .into_iter()
        .map(|p| serde_json::to_value(&p).unwrap_or_default())
        .collect();

    // Settings — gather all known keys
    let setting_keys = ["text_size", "notif_messages", "notif_posts", "notif_nearby",
                        "notif_reacts", "cache_size_bytes", "public_visible"];
    let mut settings_vec = Vec::new();
    for key in &setting_keys {
        if let Some(val) = s.get_setting(key).ok().flatten() {
            settings_vec.push((key.to_string(), val));
        }
    }

    Ok((
        if follows.is_empty() { None } else { Some(follows) },
        if profiles.is_empty() { None } else { Some(profiles) },
        if settings_vec.is_empty() { None } else { Some(settings_vec) },
    ))
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}
