use std::path::PathBuf;
use std::io::Cursor;
use anyhow::Result as AResult;
use rbxassetid::RobloxAssetId;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};

use super::{convert_texture_to_dds,get_unique_assets};
use super::{ConvertTextureError,UniqueAssets};

const DOWNLOAD_LIMIT:usize=16;

#[derive(clap::Subcommand)]
pub enum Commands{
	RobloxToSNF(RobloxToSNFSubcommand),
	DownloadAssets(DownloadAssetsSubcommand),
	ExtractUnionData(ExtractUnionData),
}

#[derive(clap::Args)]
pub struct RobloxToSNFSubcommand{
	#[arg(long)]
	output_folder:PathBuf,
	#[arg(required=true)]
	input_files:Vec<PathBuf>,
}
#[derive(clap::Args)]
pub struct DownloadAssetsSubcommand{
	#[arg(required=true)]
	roblox_files:Vec<PathBuf>,
	#[arg(long,group="cookie",required=true)]
	cookie_literal:Option<String>,
	#[arg(long,group="cookie",required=true)]
	cookie_envvar:Option<String>,
	#[arg(long,group="cookie",required=true)]
	cookie_file:Option<PathBuf>,
}
#[derive(clap::Args)]
pub struct ExtractUnionData{
	#[arg(long)]
	output_folder:PathBuf,
	#[arg(long)]
	input_file:PathBuf,
}

impl Commands{
	pub async fn run(self)->AResult<()>{
		match self{
			Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
			Commands::DownloadAssets(subcommand)=>download_assets(
				subcommand.roblox_files,
				cookie_from_args(
					subcommand.cookie_literal,
					subcommand.cookie_envvar,
					subcommand.cookie_file,
				).await?,
			).await,
			Commands::ExtractUnionData(subcommand)=>extract_union_data(subcommand.input_file,subcommand.output_folder).await,
		}
	}
}

async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<rbx_asset::cookie::Cookie>{
	let cookie=match (literal,environment,file){
		(Some(cookie_literal),None,None)=>cookie_literal,
		(None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?,
		(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
		_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?,
	};
	Ok(rbx_asset::cookie::Cookie::new(cookie))
}

enum DownloadType{
	Texture(RobloxAssetId),
	Mesh(RobloxAssetId),
	Union(RobloxAssetId),
}
impl DownloadType{
	fn path(&self)->PathBuf{
		match self{
			DownloadType::Texture(RobloxAssetId(asset_id))=>format!("downloaded_textures/{asset_id}").into(),
			DownloadType::Mesh(RobloxAssetId(asset_id))=>format!("meshes/{asset_id}").into(),
			DownloadType::Union(RobloxAssetId(asset_id))=>format!("unions/{asset_id}").into(),
		}
	}
	fn asset_id(&self)->u64{
		match self{
			&DownloadType::Texture(RobloxAssetId(asset_id))=>asset_id,
			&DownloadType::Mesh(RobloxAssetId(asset_id))=>asset_id,
			&DownloadType::Union(RobloxAssetId(asset_id))=>asset_id,
		}
	}
}
enum DownloadResult{
	Cached(PathBuf),
	Data(Vec<u8>),
	Failed,
}
#[derive(Default,Debug)]
struct Stats{
	total_assets:u32,
	cached_assets:u32,
	downloaded_assets:u32,
	failed_downloads:u32,
	timed_out_downloads:u32,
}
async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{
	stats.total_assets+=1;
	// check if file exists on disk
	let path=download_instruction.path();
	if tokio::fs::try_exists(path.as_path()).await?{
		stats.cached_assets+=1;
		return Ok(DownloadResult::Cached(path));
	}
	let asset_id=download_instruction.asset_id();
	// if not, download file
	let mut retry=0;
	let mut backoff=1000f32;
	loop{
		let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
			asset_id,
			version:None,
		}).await;
		match asset_result{
			Ok(asset_result)=>{
				stats.downloaded_assets+=1;
				let data=asset_result.to_vec()?;
				tokio::fs::write(path,&data).await?;
				break Ok(DownloadResult::Data(data));
			},
			Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
				if status_code.as_u16()==429{
					if retry==12{
						println!("Giving up asset download {asset_id}");
						stats.timed_out_downloads+=1;
						break Ok(DownloadResult::Failed);
					}
					println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
					tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
					backoff*=super::BACKOFF_MUL;
					retry+=1;
				}else{
					stats.failed_downloads+=1;
					println!("weird status_code error: status_code={status_code} url={} body={}",url_and_body.url,url_and_body.body);
					break Ok(DownloadResult::Failed);
				}
			},
			Err(e)=>{
				stats.failed_downloads+=1;
				println!("sadly error: {e}");
				break Ok(DownloadResult::Failed);
			},
		}
	}
}

async fn cli_convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),CliConvertTextureError>{
	let data=match download_result{
		DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
		DownloadResult::Data(data)=>data,
		DownloadResult::Failed=>return Ok(()),
	};
	let dds=convert_texture_to_dds(&data)?;

	let file_name=format!("textures/{asset_id}.dds");
	tokio::fs::write(file_name,&dds).await?;
	Ok(())
}
#[derive(Debug,thiserror::Error)]
enum CliConvertTextureError{
	#[error("Io error {0:?}")]
	Io(#[from]std::io::Error),
	#[error("ConvertTexture error {0:?}")]
	ConvertTexture(#[from]ConvertTextureError),
}

async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
	tokio::try_join!(
		tokio::fs::create_dir_all("downloaded_textures"),
		tokio::fs::create_dir_all("textures"),
		tokio::fs::create_dir_all("meshes"),
		tokio::fs::create_dir_all("unions"),
	)?;
	// use mpsc
	let thread_limit=std::thread::available_parallelism()?.get();
	let (send_assets,mut recv_assets)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT);
	let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
	// map decode dispatcher
	// read files multithreaded
	// produce UniqueAssetsResult per file
	tokio::spawn(async move{
		// move send so it gets dropped when all maps have been decoded
		// closing the channel
		let mut it=paths.into_iter();
		static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
		SEM.add_permits(thread_limit);
		while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
			let send=send_assets.clone();
			tokio::spawn(async move{
				let result=match tokio::fs::read(&path).await{
					Ok(data)=>super::load_dom(&data).map(|dom|get_unique_assets(&dom)).map_err(|e|format!("{e:?}")),
					Err(e)=>Err(format!("{e:?}")),
				};
				_=send.send(result).await;
				drop(permit);
			});
		}
	});
	// download manager
	let mut stats=Stats::default();
	let context=rbx_asset::cookie::Context::new(cookie);
	let mut globally_unique_assets=UniqueAssets::default();
	let download_thread=tokio::spawn(async move{
		while let Some(result)=recv_assets.recv().await{
			let unique_assets=match result{
				Ok(unique_assets)=>unique_assets,
				Err(e)=>{
					println!("error: {e:?}");
					continue;
				},
			};
			for texture_id in unique_assets.textures{
				if globally_unique_assets.textures.insert(texture_id){
					let data=download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?;
					send_texture.send((texture_id,data)).await?;
				}
			}
			for mesh_id in unique_assets.meshes{
				if globally_unique_assets.meshes.insert(mesh_id){
					download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?;
				}
			}
			for union_id in unique_assets.unions{
				if globally_unique_assets.unions.insert(union_id){
					download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?;
				}
			}
		}
		dbg!(stats);
		Ok::<(),anyhow::Error>(())
	});
	static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
	SEM.add_permits(thread_limit);
	while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
		tokio::spawn(async move{
			let result=cli_convert_texture(asset_id,download_result).await;
			drop(permit);
			result.unwrap();
		});
	}
	download_thread.await??;
	_=SEM.acquire_many(thread_limit as u32).await.unwrap();
	Ok(())
}

async fn roblox_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf)->AResult<()>{
	let start=std::time::Instant::now();

	let thread_limit=std::thread::available_parallelism()?.get();
	let mut it=paths.into_iter();
	static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
	// This is wrong! Calling roblox_to_snf multiple times keeps adding permits
	SEM.add_permits(thread_limit);

	while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
		let output_folder=output_folder.clone();
		tokio::task::spawn_blocking(move||{
			let result=std::fs::read(&path).and_then(|data|{
				super::load_dom(&data).map_err(|e|std::io::Error::other(e))
				.and_then(|dom|convert_to_snf(dom).map_err(|e|std::io::Error::other(e)))
				.and_then(|output|{
					let mut dest=output_folder;
					dest.push(path.file_stem().unwrap());
					dest.set_extension("snfm");
					std::fs::write(dest,&output.snf)?;
					Ok(output)
				})
			});
			drop(permit);
			match result{
				Ok(output)=>{
					for error in output.script_errors{
						println!("Script error: {error}");
					}
					let error_count=output.convert_errors.count();
					if error_count!=0{
						println!("Error count: {error_count}");
						println!("Errors: {}",output.convert_errors);
					}
				},
				Err(e)=>println!("Convert error: {e:?}"),
			}
		});
	}
	_=SEM.acquire_many(thread_limit as u32).await.unwrap();

	println!("elapsed={:?}", start.elapsed());
	Ok(())
}

#[expect(dead_code)]
#[derive(Debug)]
pub enum ConvertError{
	IO(std::io::Error),
	SNFMap(strafesnet_snf::map::Error),
	RobloxLoadMesh(super::loader::MeshError),
	RobloxLoadTexture(super::loader::TextureError),
}
impl std::fmt::Display for ConvertError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for ConvertError{}

pub struct ConvertOutput{
	pub snf:Vec<u8>,
	pub script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
	pub convert_errors:strafesnet_rbx_loader::RecoverableErrors,
}

pub fn convert_to_snf(dom:rbx_dom_weak::WeakDom)->Result<ConvertOutput,ConvertError>{
	const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;

	// run scripts
	let model=strafesnet_rbx_loader::Model::new(dom);

	let mut place=strafesnet_rbx_loader::Place::from(model);
	let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);

	// convert
	let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
	let mut mesh_deferred_loader=MeshDeferredLoader::new();

	let map_step1=strafesnet_rbx_loader::rbx::convert(
		place.as_ref(),
		&mut texture_deferred_loader,
		&mut mesh_deferred_loader,
	);

	let mut mesh_loader=super::loader::MeshLoader::new();
	let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadMesh)?;

	let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);

	let mut texture_loader=super::loader::TextureLoader::new();
	let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadTexture)?;

	let (map,convert_errors)=map_step2.add_render_configs_and_textures(render_configs);

	let mut snf_buf=Vec::new();
	strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;

	Ok(ConvertOutput{
		snf:snf_buf,
		script_errors,
		convert_errors,
	})
}

async fn extract_union_data(input_file:PathBuf,output_folder:PathBuf)->AResult<()>{
	let data=tokio::fs::read(&input_file).await?;

	let dom=rbx_binary::from_reader(Cursor::new(data))?;
	let &[referent]=dom.root().children()else{
		panic!("Expected one child");
	};
	let Some(instance)=dom.get_by_ref(referent)else{
		panic!("Missing instance");
	};
	let output_file=output_folder.join(input_file.file_stem().unwrap());
	if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("PhysicsData")){
		let mut physics_data_path=output_file.clone();
		physics_data_path.set_extension("physicsdata");
		tokio::fs::write(physics_data_path,data).await?;
	}
	if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("MeshData")){
		let mut mesh_data_path=output_file.clone();
		mesh_data_path.set_extension("meshdata");
		tokio::fs::write(mesh_data_path,data).await?;
	}

	Ok(())
}
