use aws_sdk_s3::Client;
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::operation::copy_object::CopyObjectError;
use aws_sdk_s3::operation::delete_object::DeleteObjectError;
use aws_sdk_s3::operation::get_object::GetObjectError;
use aws_sdk_s3::operation::head_object::HeadObjectError;
use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
use aws_sdk_s3::operation::put_object::PutObjectError;
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::primitives::ByteStream;
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;

use crate::aws::config;

/// S3ストレージの操作中に発生する可能性のあるエラーです
#[derive(Debug)]
pub enum S3Error {
	/// オブジェクト取得エラー
	GetObject(SdkError<GetObjectError>),
	/// オブジェクト書き込みエラー
	PutObject(SdkError<PutObjectError>),
	/// メタデータ取得エラー
	HeadObject(SdkError<HeadObjectError>),
	/// オブジェクト削除エラー
	DeleteObject(SdkError<DeleteObjectError>),
	/// オブジェクトコピーエラー
	CopyObject(SdkError<CopyObjectError>),
	/// オブジェクトリスト取得エラー
	ListObjects(SdkError<ListObjectsV2Error>),
	/// 署名設定エラー
	PresigningConfig(String),
	/// 署名生成エラー（GetObject用）
	PresignGet(SdkError<GetObjectError>),
	/// 署名生成エラー（PutObject用）
	PresignPut(SdkError<PutObjectError>),
	/// その他のエラー
	Other(String),
}

impl std::error::Error for S3Error {}

impl fmt::Display for S3Error {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			S3Error::GetObject(e) => write!(f, "GetObject error: {}", e),
			S3Error::PutObject(e) => write!(f, "PutObject error: {}", e),
			S3Error::HeadObject(e) => write!(f, "HeadObject error: {}", e),
			S3Error::DeleteObject(e) => write!(f, "DeleteObject error: {}", e),
			S3Error::CopyObject(e) => write!(f, "CopyObject error: {}", e),
			S3Error::ListObjects(e) => write!(f, "ListObjects error: {}", e),
			S3Error::PresigningConfig(m) => write!(f, "Presigning config error: {}", m),
			S3Error::PresignGet(e) => write!(f, "Presign GET error: {}", e),
			S3Error::PresignPut(e) => write!(f, "Presign PUT error: {}", e),
			S3Error::Other(m) => write!(f, "Other error: {}", m),
		}
	}
}

/// S3ストレージの操作をラップする構造体です
pub struct S3Storage {
	/// S3バケット名
	pub bucket: String,
	/// S3クライアント
	pub client: Client,
}

/// S3オブジェクトのメタ情報を保持する構造体です
#[derive(Debug)]
pub struct ObjectMeta {
	/// コンテンツタイプ
	pub content_type: Option<String>,
	/// メタデータ
	pub metadata: HashMap<String, String>,
	/// キャッシュ制御
	pub cache_control: Option<String>,
	/// コンテンツ長
	pub content_length: i64,
}

/// オブジェクトリストのページ情報を保持する構造体です
#[derive(Debug)]
pub struct Page {
	/// キーのリスト
	pub keys: Vec<String>,
	/// 次のページを取得するためのトークン
	pub next_continuation_token: Option<String>,
}

impl S3Storage {
	/// 新しいS3Storageインスタンスを作成します
	pub async fn new(bucket: &str) -> Self {
		// 1. 設定読み込み（環境変数 / プロファイル など）
		let sdk_config = config().await;
		let client = Client::new(&sdk_config);
		Self { bucket: bucket.to_string(), client }
	}

	/// 指定されたキーのデータを読み込みます
	pub async fn read(&self, key: &str) -> Result<Vec<u8>, SdkError<GetObjectError>> {
		let resp = self
			.client
			.get_object()
			.bucket(&self.bucket)
			.key(key)
			.send()
			.await?;
		let data = resp
			.body
			.collect()
			.await
			.map_err(|e| SdkError::construction_failure(e))?
			.to_vec();
		Ok(data)
	}

	/// 指定されたキーにデータを書き込みます
	pub async fn write(
		&self,
		key: &str,
		bytes: Vec<u8>,
		content_type: Option<String>,
		metadata: Option<HashMap<String, String>>,
		cache_control: Option<String>,
	) -> Result<(), SdkError<PutObjectError>> {
		let mut builder = self
			.client
			.put_object()
			.bucket(&self.bucket)
			.key(key)
			.body(ByteStream::from(bytes));

		if let Some(ct) = content_type {
			builder = builder.content_type(ct);
		}
		if let Some(cc) = cache_control {
			builder = builder.cache_control(cc);
		}
		if let Some(meta) = metadata {
			for (k, v) in meta {
				builder = builder.metadata(k, v);
			}
		}

		builder.send().await?;
		Ok(())
	}

	/// 指定されたキーのメタデータを取得します
	pub async fn head(&self, key: &str) -> Result<ObjectMeta, SdkError<HeadObjectError>> {
		let resp = self
			.client
			.head_object()
			.bucket(&self.bucket)
			.key(key)
			.send()
			.await?;

		Ok(ObjectMeta {
			content_type: resp.content_type,
			metadata: resp.metadata.unwrap_or_default(),
			cache_control: resp.cache_control,
			content_length: resp.content_length.unwrap_or(0),
		})
	}

	/// 指定されたキーのオブジェクトを削除します
	pub async fn remove(&self, key: &str) -> Result<(), SdkError<DeleteObjectError>> {
		self.client
			.delete_object()
			.bucket(&self.bucket)
			.key(key)
			.send()
			.await?;
		Ok(())
	}

	/// オブジェクトをコピーします
	pub async fn copy(&self, src: &str, dst: &str) -> Result<(), SdkError<CopyObjectError>> {
		let copy_source = format!("{}/{}", self.bucket, src);
		self.client
			.copy_object()
			.bucket(&self.bucket)
			.copy_source(copy_source)
			.key(dst)
			.send()
			.await?;
		Ok(())
	}

	/// オブジェクトを移動（リネーム）します（コピーして元のオブジェクトを削除）
	pub async fn relocate(&self, src: &str, dst: &str) -> Result<(), S3Error> {
		self.copy(src, dst).await.map_err(S3Error::CopyObject)?;
		self.remove(src).await.map_err(S3Error::DeleteObject)?;
		Ok(())
	}

	/// オブジェクトの一覧を取得します
	pub async fn list(
		&self,
		prefix: Option<String>,
		continuation: Option<String>,
	) -> Result<Page, SdkError<ListObjectsV2Error>> {
		let mut builder = self.client.list_objects_v2().bucket(&self.bucket);

		if let Some(p) = prefix {
			builder = builder.prefix(p);
		}
		if let Some(c) = continuation {
			builder = builder.continuation_token(c);
		}

		let resp = builder.send().await?;
		let keys = resp
			.contents
			.unwrap_or_default()
			.into_iter()
			.filter_map(|obj| obj.key)
			.collect();

		Ok(Page {
			keys,
			next_continuation_token: resp.next_continuation_token,
		})
	}

	/// 読み取り用の署名付きURLを生成します
	pub async fn presign_read_url(
		&self,
		key: &str,
		expires_in: Duration,
	) -> Result<String, S3Error> {
		let config = PresigningConfig::builder()
			.expires_in(expires_in)
			.build()
			.map_err(|e| S3Error::PresigningConfig(e.to_string()))?;

		let presigned_req = self
			.client
			.get_object()
			.bucket(&self.bucket)
			.key(key)
			.presigned(config)
			.await
			.map_err(S3Error::PresignGet)?;

		Ok(presigned_req.uri().to_string())
	}

	/// 書き込み用の署名付きURLを生成します
	pub async fn presign_write_url(
		&self,
		key: &str,
		expires_in: Duration,
	) -> Result<String, S3Error> {
		let config = PresigningConfig::builder()
			.expires_in(expires_in)
			.build()
			.map_err(|e| S3Error::PresigningConfig(e.to_string()))?;

		let presigned_req = self
			.client
			.put_object()
			.bucket(&self.bucket)
			.key(key)
			.presigned(config)
			.await
			.map_err(S3Error::PresignPut)?;

		Ok(presigned_req.uri().to_string())
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use dotenvy::dotenv;
	use std::env;

	/// テスト用のS3Storageインスタンスを作成します
	async fn setup() -> Option<S3Storage> {
		dotenv().ok();
		let bucket = env::var("TEST_S3_BUCKET")
			.ok()
			.unwrap_or("surfic-storage".to_string());
		Some(S3Storage::new(&bucket).await)
	}

	/// 全ての基本操作（書き込み、読み込み、コピー、移動、一覧取得、削除）をテストします
	#[tokio::test]
	async fn test_all_operations() -> Result<(), Box<dyn std::error::Error>> {
		let s3 = match setup().await {
			Some(s3) => s3,
			None => {
				println!("TEST_S3_BUCKET が設定されていないためテストをスキップします");
				return Ok(());
			}
		};

		let test_key = "test/hello.txt";
		let copy_key = "test/hello_copy.txt";
		let move_key = "test/hello_moved.txt";
		let content = b"Hello, S3 Storage!".to_vec();

		// 1. 書き込み
		println!("Writing...");
		s3.write(
			test_key,
			content.clone(),
			Some("text/plain".into()),
			None,
			None,
		)
		.await?;

		// 2. 読み込み
		println!("Reading...");
		let read_data = s3.read(test_key).await?;
		assert_eq!(&read_data, &content);

		// 3. メタデータ取得
		println!("Heading...");
		let meta = s3.head(test_key).await?;
		assert_eq!(meta.content_length, content.len() as i64);

		// 4. コピー
		println!("Copying...");
		s3.copy(test_key, copy_key).await?;
		let copied_data = s3.read(copy_key).await?;
		assert_eq!(&copied_data, &content);

		// 5. リスト取得
		println!("Listing...");
		let page = s3.list(Some("test/".into()), None).await?;
		assert!(page.keys.contains(&test_key.to_string()));
		assert!(page.keys.contains(&copy_key.to_string()));

		// 6. 移動 (copy + delete)
		println!("Moving...");
		s3.relocate(copy_key, move_key).await?;
		let moved_data = s3.read(move_key).await?;
		assert_eq!(&moved_data, &content);

		// 元の場所には無いはず
		let list_after_move = s3.list(Some("test/".into()), None).await?;
		assert!(!list_after_move.keys.contains(&copy_key.to_string()));

		// 7. 削除
		println!("Removing...");
		s3.remove(test_key).await?;
		s3.remove(move_key).await?;

		// 削除後の確認
		let final_list = s3.list(Some("test/".into()), None).await?;
		assert!(!final_list.keys.contains(&test_key.to_string()));
		assert!(!final_list.keys.contains(&move_key.to_string()));

		println!("Test completed successfully!");
		Ok(())
	}

	/// 署名付きURLの生成をテストします
	#[tokio::test]
	async fn test_presign_urls() -> Result<(), Box<dyn std::error::Error>> {
		let s3 = match setup().await {
			Some(s3) => s3,
			None => return Ok(()),
		};

		let key = "test/presign.txt";
		let expires = Duration::from_secs(3600);

		let read_url = s3.presign_read_url(key, expires).await?;
		println!("Read URL: {}", read_url);
		assert!(read_url.contains(&s3.bucket));
		assert!(read_url.contains(key));

		let write_url = s3.presign_write_url(key, expires).await?;
		println!("Write URL: {}", write_url);
		assert!(write_url.contains(&s3.bucket));
		assert!(write_url.contains(key));

		Ok(())
	}
}
