use anyhow::{Context, Result};
use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use rayon::prelude::*;
use std::path::Path;
use std::sync::atomic::{AtomicU32, Ordering};

use crate::config::Config;
use crate::crypto::Cipher;
use crate::manifest::Manifest;
use crate::merge::smart_merge;
use crate::parser::{entries_to_jsonl, parse_jsonl, parse_jsonl_file};
use crate::resolver::{build_remote_map, resolve_project_dir};
use crate::scanner::{LocalSession, ScanFilter, scan_sessions};
use crate::storage::StorageProvider;

fn is_safe_path_component(s: &str) -> bool {
    if s.contains('/') || s.contains('\\') {
        return false;
    }
    !s.split('-').any(|seg| seg == "..")
}

fn session_filename(uuid: &str, encrypted: bool) -> String {
    if encrypted {
        format!("{uuid}.jsonl.age")
    } else {
        format!("{uuid}.jsonl")
    }
}

fn is_encrypted(config: &Config) -> bool {
    !matches!(config.encryption, crate::config::EncryptionConfig::None)
}

pub fn push(
    config: &Config,
    cipher: &Cipher,
    filter: &ScanFilter,
    storage: &dyn StorageProvider,
) -> Result<PushResult> {
    let manifest_rel = manifest_filename(config);
    let mut manifest = if storage.exists(&manifest_rel) {
        let data = storage.read_file(&manifest_rel)?;
        let plain = cipher.decrypt(&data)?;
        serde_json::from_slice(&plain)?
    } else {
        Manifest::new()
    };

    let local_sessions = scan_sessions(&config.claude_projects_dir(), filter)?;
    let encrypted = is_encrypted(config);
    let include_companions = config.sync.include_companion_dirs;

    let to_push: Vec<&LocalSession> = local_sessions
        .iter()
        .filter(|s| {
            manifest
                .sessions
                .get(&s.uuid)
                .is_none_or(|existing| existing.content_hash != s.entry.content_hash)
        })
        .collect();

    let skipped = (local_sessions.len() - to_push.len()) as u32;

    let results: Vec<_> = to_push
        .par_iter()
        .map(|session| {
            let result = push_session(session, cipher, storage, encrypted, include_companions);
            (session.uuid.clone(), session.entry.clone(), result)
        })
        .collect();

    let mut push_count = 0u32;
    for (uuid, entry, result) in results {
        match result {
            Ok(()) => {
                manifest.sessions.insert(uuid, entry);
                push_count += 1;
            }
            Err(e) => eprintln!("warning: {uuid}: {e}"),
        }
    }

    let manifest_json = serde_json::to_string_pretty(&manifest)?;
    let manifest_data = cipher.encrypt(manifest_json.as_bytes())?;
    let tmp_manifest = format!("{manifest_rel}.tmp");
    storage.write_file(&tmp_manifest, &manifest_data)?;
    let root = storage.root();
    std::fs::rename(root.join(&tmp_manifest), root.join(&manifest_rel))?;

    Ok(PushResult {
        pushed: push_count,
        skipped,
    })
}

fn push_session(
    session: &LocalSession,
    cipher: &Cipher,
    storage: &dyn StorageProvider,
    encrypted: bool,
    include_companions: bool,
) -> Result<()> {
    let filename = session_filename(&session.uuid, encrypted);
    let rel_path = format!("sessions/{filename}");
    let plaintext = std::fs::read(&session.jsonl_path)
        .with_context(|| format!("reading session {}", session.uuid))?;
    let data = cipher
        .encrypt(&plaintext)
        .with_context(|| format!("encrypting session {}", session.uuid))?;
    storage.write_file(&rel_path, &data)?;

    if include_companions && let Some(ref companion) = session.companion_dir {
        let tar_data = tar_directory(companion)?;
        let data = cipher.encrypt(&tar_data)?;
        let ext = if encrypted { "tar.gz.age" } else { "tar.gz" };
        storage.write_file(&format!("sessions/{}.dir.{ext}", session.uuid), &data)?;
    }

    Ok(())
}

enum PullAction {
    New {
        uuid: String,
        project_dir_name: String,
        rel_path: String,
    },
    Merge {
        uuid: String,
        project_dir_name: String,
        rel_path: String,
        local_jsonl_path: std::path::PathBuf,
    },
}

pub fn pull(
    config: &Config,
    cipher: &Cipher,
    filter: &ScanFilter,
    storage: &dyn StorageProvider,
) -> Result<PullResult> {
    let manifest_rel = manifest_filename(config);
    if !storage.exists(&manifest_rel) {
        return Ok(PullResult {
            pulled: 0,
            merged: 0,
            skipped: 0,
        });
    }

    let manifest_data = storage.read_file(&manifest_rel)?;
    let manifest_plain = cipher.decrypt(&manifest_data)?;
    let remote_manifest: Manifest = serde_json::from_slice(&manifest_plain)?;

    let projects_dir = config.claude_projects_dir();
    let local_sessions = scan_sessions(&projects_dir, filter)?;
    let local_map: std::collections::HashMap<String, &LocalSession> =
        local_sessions.iter().map(|s| (s.uuid.clone(), s)).collect();

    let remote_map = build_remote_map(&projects_dir);
    let encrypted = is_encrypted(config);

    let mut actions: Vec<PullAction> = Vec::new();
    let mut skipped = 0u32;

    for (uuid, remote_entry) in &remote_manifest.sessions {
        if !is_safe_path_component(uuid) || !is_safe_path_component(&remote_entry.project_path) {
            eprintln!("warning: skipping session with unsafe path: {uuid}");
            skipped += 1;
            continue;
        }

        let filename = session_filename(uuid, encrypted);
        let rel_path = format!("sessions/{filename}");

        if !storage.exists(&rel_path) {
            eprintln!("warning: session {uuid} not found in repo, skipping");
            skipped += 1;
            continue;
        }

        let project_dir_name =
            resolve_project_dir(&remote_entry.project_path, &remote_map, &projects_dir)
                .unwrap_or_else(|| {
                    crate::manifest::denormalize_project_path(&remote_entry.project_path)
                });

        if let Some(local) = local_map.get(uuid) {
            if local.entry.content_hash == remote_entry.content_hash {
                skipped += 1;
                continue;
            }
            actions.push(PullAction::Merge {
                uuid: uuid.clone(),
                project_dir_name,
                rel_path,
                local_jsonl_path: local.jsonl_path.clone(),
            });
        } else {
            actions.push(PullAction::New {
                uuid: uuid.clone(),
                project_dir_name,
                rel_path,
            });
        }
    }

    let pulled = AtomicU32::new(0);
    let merged = AtomicU32::new(0);

    let errors: Vec<_> = actions
        .par_iter()
        .filter_map(|action| {
            let result = match action {
                PullAction::New {
                    uuid,
                    project_dir_name,
                    rel_path,
                } => {
                    let r = pull_new(
                        uuid,
                        project_dir_name,
                        rel_path,
                        cipher,
                        storage,
                        &projects_dir,
                        encrypted,
                    );
                    if r.is_ok() {
                        pulled.fetch_add(1, Ordering::Relaxed);
                    }
                    r
                }
                PullAction::Merge {
                    uuid,
                    project_dir_name,
                    rel_path,
                    local_jsonl_path,
                } => {
                    let r = pull_merge(
                        uuid,
                        project_dir_name,
                        rel_path,
                        local_jsonl_path,
                        cipher,
                        storage,
                        &projects_dir,
                    );
                    if r.is_ok() {
                        merged.fetch_add(1, Ordering::Relaxed);
                    }
                    r
                }
            };
            match result {
                Ok(()) => None,
                Err(e) => Some(format!("{e}")),
            }
        })
        .collect();

    for err in &errors {
        eprintln!("warning: {err}");
    }

    Ok(PullResult {
        pulled: pulled.load(Ordering::Relaxed),
        merged: merged.load(Ordering::Relaxed),
        skipped,
    })
}

fn pull_new(
    uuid: &str,
    project_dir_name: &str,
    rel_path: &str,
    cipher: &Cipher,
    storage: &dyn StorageProvider,
    projects_dir: &Path,
    encrypted: bool,
) -> Result<()> {
    let data = storage.read_file(rel_path)?;
    let plaintext = cipher.decrypt(&data)?;
    let target_dir = projects_dir.join(project_dir_name);
    std::fs::create_dir_all(&target_dir)?;
    std::fs::write(target_dir.join(format!("{uuid}.jsonl")), plaintext)?;

    let ext = if encrypted { "tar.gz.age" } else { "tar.gz" };
    let companion_rel = format!("sessions/{uuid}.dir.{ext}");
    if storage.exists(&companion_rel) {
        let tar_data = storage.read_file(&companion_rel)?;
        let plain_tar = cipher.decrypt(&tar_data)?;
        let companion_dir = target_dir.join(uuid);
        untar_directory(&plain_tar, &companion_dir)?;
    }

    Ok(())
}

fn pull_merge(
    uuid: &str,
    project_dir_name: &str,
    rel_path: &str,
    local_jsonl_path: &Path,
    cipher: &Cipher,
    storage: &dyn StorageProvider,
    projects_dir: &Path,
) -> Result<()> {
    let remote_data = storage.read_file(rel_path)?;
    let remote_plain = cipher.decrypt(&remote_data)?;
    let remote_entries = parse_jsonl(&remote_plain)?;
    let local_entries = parse_jsonl_file(local_jsonl_path)?;

    let merge_result = smart_merge(&local_entries, &remote_entries);
    let merged_data = entries_to_jsonl(&merge_result.entries)?;

    let target_dir = projects_dir.join(project_dir_name);
    std::fs::create_dir_all(&target_dir)?;
    std::fs::write(target_dir.join(format!("{uuid}.jsonl")), merged_data)?;
    Ok(())
}

pub fn status(
    config: &Config,
    cipher: &Cipher,
    filter: &ScanFilter,
    storage: &dyn StorageProvider,
) -> Result<StatusResult> {
    let manifest_rel = manifest_filename(config);
    let remote_manifest = if storage.exists(&manifest_rel) {
        let data = storage.read_file(&manifest_rel)?;
        let plain = cipher.decrypt(&data)?;
        serde_json::from_slice(&plain)?
    } else {
        Manifest::new()
    };

    let local_sessions = scan_sessions(&config.claude_projects_dir(), filter)?;
    let local_map: std::collections::HashMap<&str, &LocalSession> = local_sessions
        .iter()
        .map(|s| (s.uuid.as_str(), s))
        .collect();

    let mut local_only = Vec::new();
    let mut remote_only = Vec::new();
    let mut diverged = Vec::new();
    let mut in_sync = 0u32;

    for session in &local_sessions {
        match remote_manifest.sessions.get(&session.uuid) {
            None => local_only.push(SessionInfo {
                uuid: session.uuid.clone(),
                project: session.entry.project_path.clone(),
                size: session.entry.size,
            }),
            Some(remote) => {
                if session.entry.content_hash == remote.content_hash {
                    in_sync += 1;
                } else {
                    diverged.push(SessionInfo {
                        uuid: session.uuid.clone(),
                        project: session.entry.project_path.clone(),
                        size: session.entry.size,
                    });
                }
            }
        }
    }

    for (uuid, entry) in &remote_manifest.sessions {
        if !local_map.contains_key(uuid.as_str()) {
            remote_only.push(SessionInfo {
                uuid: uuid.clone(),
                project: entry.project_path.clone(),
                size: entry.size,
            });
        }
    }

    Ok(StatusResult {
        local_only,
        remote_only,
        diverged,
        in_sync,
    })
}

fn manifest_filename(config: &Config) -> String {
    if is_encrypted(config) {
        "manifest.json.age".into()
    } else {
        "manifest.json".into()
    }
}

pub struct PushResult {
    pub pushed: u32,
    pub skipped: u32,
}

pub struct PullResult {
    pub pulled: u32,
    pub merged: u32,
    pub skipped: u32,
}

pub struct SessionInfo {
    pub uuid: String,
    pub project: String,
    pub size: u64,
}

pub struct StatusResult {
    pub local_only: Vec<SessionInfo>,
    pub remote_only: Vec<SessionInfo>,
    pub diverged: Vec<SessionInfo>,
    pub in_sync: u32,
}

fn tar_directory(dir: &Path) -> Result<Vec<u8>> {
    let mut buf = Vec::new();
    {
        let encoder = GzEncoder::new(&mut buf, Compression::fast());
        let mut archive = tar::Builder::new(encoder);
        archive.follow_symlinks(false);
        let dir_name = dir
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| "data".to_string());
        archive.append_dir_all(&dir_name, dir)?;
        archive.into_inner()?.finish()?;
    }
    Ok(buf)
}

fn untar_directory(data: &[u8], target: &Path) -> Result<()> {
    let decoder = GzDecoder::new(data);
    let mut archive = tar::Archive::new(decoder);
    if target.exists() {
        std::fs::remove_dir_all(target)?;
    }
    if let Some(parent) = target.parent() {
        std::fs::create_dir_all(parent)?;
    }
    archive.unpack(target.parent().unwrap_or(target))?;
    Ok(())
}
