//! Core data types for s3bolt.

use std::fmt;
use std::time::Duration;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::error::{Result, S3boltError};

/// A parsed S3 URI: `s3://bucket/key`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct S3Uri {
    pub bucket: String,
    pub key: String,
}

impl S3Uri {
    /// Parse an S3 URI string into bucket and key components.
    ///
    /// Accepts `s3://bucket/key` or `s3://bucket/prefix/`.
    pub fn parse(uri: &str) -> Result<Self> {
        let stripped = uri
            .strip_prefix("s3://")
            .ok_or_else(|| S3boltError::InvalidUri {
                uri: uri.to_owned(),
            })?;

        let (bucket, key) = stripped.split_once('/').unwrap_or((stripped, ""));

        if bucket.is_empty() {
            return Err(S3boltError::InvalidUri {
                uri: uri.to_owned(),
            });
        }

        Ok(Self {
            bucket: bucket.to_owned(),
            key: key.to_owned(),
        })
    }

    /// Join a relative key suffix onto this URI treated as a prefix.
    pub fn join(&self, suffix: &str) -> Self {
        let key = if self.key.is_empty() {
            suffix.to_owned()
        } else if self.key.ends_with('/') {
            format!("{}{}", self.key, suffix)
        } else {
            format!("{}/{}", self.key, suffix)
        };

        Self {
            bucket: self.bucket.clone(),
            key,
        }
    }

    /// Returns `true` if this URI looks like a prefix (ends with `/` or has no key).
    pub fn is_prefix(&self) -> bool {
        self.key.is_empty() || self.key.ends_with('/')
    }
}

impl fmt::Display for S3Uri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "s3://{}/{}", self.bucket, self.key)
    }
}

/// Metadata about an S3 object obtained from listing.
#[derive(Debug, Clone)]
pub struct ObjectInfo {
    pub key: String,
    pub size: u64,
    pub e_tag: Option<String>,
    pub last_modified: Option<DateTime<Utc>>,
    pub storage_class: Option<String>,
}

/// A unit of work: copy one object from source to destination.
#[derive(Debug, Clone)]
pub struct CopyJob {
    pub source: S3Uri,
    pub destination: S3Uri,
    pub source_info: ObjectInfo,
    pub copy_strategy: CopyStrategy,
}

/// How to perform the copy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CopyStrategy {
    /// Single `CopyObject` API call (objects ≤ 5 GiB).
    ServerSideSingle,
    /// Multipart server-side copy (objects > 5 GiB).
    ServerSideMultipart { part_size: u64 },
}

const FIVE_GIB: u64 = 5 * 1024 * 1024 * 1024;
const MIN_PART_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB
const MAX_PART_SIZE: u64 = FIVE_GIB;
const MAX_PARTS: u64 = 10_000;

impl CopyStrategy {
    /// Choose the optimal copy strategy for a given object size.
    pub fn for_size(size: u64) -> Self {
        if size <= FIVE_GIB {
            Self::ServerSideSingle
        } else {
            let part_size = ((size / MAX_PARTS) + 1).clamp(MIN_PART_SIZE, MAX_PART_SIZE);
            Self::ServerSideMultipart { part_size }
        }
    }
}

/// Result of a single copy operation.
#[derive(Debug, Clone)]
pub struct CopyResult {
    pub source_key: String,
    pub destination_key: String,
    pub outcome: CopyOutcome,
    pub duration: Duration,
    pub bytes_copied: u64,
}

/// Outcome of a copy attempt.
#[derive(Debug, Clone)]
pub enum CopyOutcome {
    /// Copy completed successfully.
    Success { dest_e_tag: String },
    /// Object was skipped.
    Skipped { reason: SkipReason },
    /// Copy failed.
    Failed {
        error: String,
        retries_exhausted: bool,
    },
}

/// Reason an object was skipped.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkipReason {
    /// Destination already has an identical object (sync mode).
    AlreadyExists,
    /// Object was filtered out by user-specified filters.
    FilteredOut,
    /// Dry-run mode — no actual copy performed.
    DryRun,
    /// Object was already completed in a previous checkpoint.
    AlreadyCheckpointed,
}

impl fmt::Display for SkipReason {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::AlreadyExists => write!(f, "already exists at destination"),
            Self::FilteredOut => write!(f, "filtered out"),
            Self::DryRun => write!(f, "dry run"),
            Self::AlreadyCheckpointed => write!(f, "already checkpointed"),
        }
    }
}

/// Summary of an entire copy session.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CopyManifest {
    pub total_objects: u64,
    pub total_bytes: u64,
    pub copied_objects: u64,
    pub copied_bytes: u64,
    pub skipped_objects: u64,
    pub failed_objects: u64,
    pub duration_secs: f64,
    pub failed_keys: Vec<String>,
}

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

    #[test]
    fn parse_valid_uri() {
        let uri = S3Uri::parse("s3://my-bucket/path/to/file.txt").unwrap();
        assert_eq!(uri.bucket, "my-bucket");
        assert_eq!(uri.key, "path/to/file.txt");
    }

    #[test]
    fn parse_prefix_uri() {
        let uri = S3Uri::parse("s3://my-bucket/prefix/").unwrap();
        assert_eq!(uri.bucket, "my-bucket");
        assert_eq!(uri.key, "prefix/");
        assert!(uri.is_prefix());
    }

    #[test]
    fn parse_bucket_only() {
        let uri = S3Uri::parse("s3://my-bucket").unwrap();
        assert_eq!(uri.bucket, "my-bucket");
        assert_eq!(uri.key, "");
        assert!(uri.is_prefix());
    }

    #[test]
    fn parse_invalid_uri() {
        assert!(S3Uri::parse("https://bucket/key").is_err());
        assert!(S3Uri::parse("s3://").is_err());
        assert!(S3Uri::parse("s3:///key").is_err());
    }

    #[test]
    fn join_suffix() {
        let base = S3Uri::parse("s3://bucket/prefix/").unwrap();
        let joined = base.join("file.txt");
        assert_eq!(joined.to_string(), "s3://bucket/prefix/file.txt");
    }

    #[test]
    fn strategy_small_file() {
        assert_eq!(CopyStrategy::for_size(1024), CopyStrategy::ServerSideSingle);
    }

    #[test]
    fn strategy_exactly_5gib() {
        assert_eq!(
            CopyStrategy::for_size(FIVE_GIB),
            CopyStrategy::ServerSideSingle
        );
    }

    #[test]
    fn strategy_large_file() {
        let strategy = CopyStrategy::for_size(FIVE_GIB + 1);
        match strategy {
            CopyStrategy::ServerSideMultipart { part_size } => {
                assert!(part_size >= MIN_PART_SIZE);
                assert!(part_size <= MAX_PART_SIZE);
            }
            _ => panic!("expected multipart strategy"),
        }
    }

    #[test]
    fn strategy_very_large_file() {
        // 5 TiB (S3 max object size)
        let size = 5 * 1024 * 1024 * 1024 * 1024_u64;
        let strategy = CopyStrategy::for_size(size);
        match strategy {
            CopyStrategy::ServerSideMultipart { part_size } => {
                let num_parts = size.div_ceil(part_size);
                assert!(num_parts <= MAX_PARTS);
            }
            _ => panic!("expected multipart strategy"),
        }
    }

    #[test]
    fn display_uri() {
        let uri = S3Uri {
            bucket: "b".to_owned(),
            key: "k".to_owned(),
        };
        assert_eq!(uri.to_string(), "s3://b/k");
    }
}
