use crate::download::download_asset_version;
use crate::rbx_util::{class_is_a,get_mapinfo,get_root_instance,read_dom,MapInfo,ReadDomError};

use heck::{ToSnakeCase,ToTitleCase};

#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
	ModelInfoDownload(rbx_asset::cloud::GetError),
	CreatorTypeMustBeUser,
	Download(crate::download::Error),
	ModelFileDecode(ReadDomError),
}
impl std::fmt::Display for Error{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for Error{}

#[allow(nonstandard_style)]
pub struct CheckRequest{
	pub ModelID:u64,
}

impl From<crate::nats_types::CheckMapfixRequest> for CheckRequest{
	fn from(value:crate::nats_types::CheckMapfixRequest)->Self{
		Self{
			ModelID:value.ModelID,
		}
	}
}
impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
	fn from(value:crate::nats_types::CheckSubmissionRequest)->Self{
		Self{
			ModelID:value.ModelID,
		}
	}
}

#[derive(Default)]
pub enum Check{
	Pass,
	#[default]
	Fail,
}
impl Check{
	fn pass(&self)->bool{
		match self{
			Check::Pass=>true,
			Check::Fail=>false,
		}
	}
}
impl std::fmt::Display for Check{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		match self{
			Check::Pass=>write!(f,"passed"),
			Check::Fail=>write!(f,"failed"),
		}
	}
}

#[derive(Default)]
pub struct CheckReport{
	// === METADATA CHECKS ===
	// the model must have exactly 1 root part (models uploaded to roblox can have multiple roots)
	exactly_one_root:Check,
	// the root must be of class Model
	root_is_model:Check,
	// the prefix of the model's name must match the game it was submitted for. bhop_ for bhop, and surf_ for surf
	model_name_prefix_is_valid:Check,
	// your model's name must match this regex: ^[a-z0-9_]
	model_name_is_snake_case:Check,
	// map must have a StringValue named Creator and DisplayName. additionally, they must both have a value
	has_display_name:Check,
	has_creator:Check,
	// the display name must be capitalized
	display_name_is_title_case:Check,
	// you cannot have any Scripts or ModuleScripts that have the keyword 'getfenv" or 'require'
	// you cannot have more than 50 duplicate scripts

	// === MODE CHECKS ===
	// Exactly one MapStart
	exactly_one_mapstart:Check,
	// At least one MapFinish
	at_least_one_mapfinish:Check,
	// Spawn0 or Spawn1 must exist
	spawn1_exists:Check,
	// No duplicate Spawn#
	no_duplicate_spawns:Check,
	// No duplicate WormholeOut# (duplicate WormholeIn# ok)
	no_duplicate_wormhole_out:Check,
}
impl CheckReport{
	pub fn pass(&self)->bool{
		return self.exactly_one_root.pass()
			 &&self.root_is_model.pass()
			 &&self.model_name_prefix_is_valid.pass()
			 &&self.model_name_is_snake_case.pass()
			 &&self.has_display_name.pass()
			 &&self.has_creator.pass()
			 &&self.display_name_is_title_case.pass()
			 &&self.exactly_one_mapstart.pass()
			 &&self.at_least_one_mapfinish.pass()
			 &&self.spawn1_exists.pass()
			 &&self.no_duplicate_spawns.pass()
			 &&self.no_duplicate_wormhole_out.pass()
	}
}
impl std::fmt::Display for CheckReport{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,
			"exactly_one_root={}\n
root_is_model={}\n
model_name_prefix_is_valid={}\n
model_name_is_snake_case={}\n
has_display_name={}\n
has_creator={}\n
display_name_is_title_case={}\n
exactly_one_mapstart={}\n
at_least_one_mapfinish={}\n
spawn1_exists={}\n
no_duplicate_spawns={}\n
no_duplicate_wormhole_out={}",
			self.exactly_one_root,
			self.root_is_model,
			self.model_name_prefix_is_valid,
			self.model_name_is_snake_case,
			self.has_display_name,
			self.has_creator,
			self.display_name_is_title_case,
			self.exactly_one_mapstart,
			self.at_least_one_mapfinish,
			self.spawn1_exists,
			self.no_duplicate_spawns,
			self.no_duplicate_wormhole_out,
		)
	}
}

enum Zone{
	Start(ModeID),
	Finish(ModeID),
}

#[derive(Debug,Hash,Eq,PartialEq)]
struct ModeID(u64);
impl ModeID{
	const MAIN:Self=Self(0);
	const BONUS:Self=Self(1);
}
#[allow(dead_code)]
pub enum ZoneParseError{
	NoCaptures,
	ParseInt(core::num::ParseIntError)
}
impl std::str::FromStr for Zone{
	type Err=ZoneParseError;
	fn from_str(s:&str)->Result<Self,Self::Err>{
		match s{
			"MapStart"=>Ok(Self::Start(ModeID::MAIN)),
			"MapFinish"=>Ok(Self::Finish(ModeID::MAIN)),
			"BonusStart"=>Ok(Self::Start(ModeID::BONUS)),
			"BonusFinish"=>Ok(Self::Finish(ModeID::BONUS)),
			other=>{
				let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
				if let Some(captures)=bonus_start_pattern.captures(other){
					return Ok(Self::Start(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?)));
				}
				let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
				if let Some(captures)=bonus_finish_pattern.captures(other){
					return Ok(Self::Finish(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?)));
				}
				Err(ZoneParseError::NoCaptures)
			}
		}
	}
}


#[derive(Debug,Hash,Eq,PartialEq)]
struct SpawnID(u64);
impl SpawnID{
	const FIRST:Self=Self(1);
}
#[derive(Debug,Hash,Eq,PartialEq)]
struct WormholeOutID(u64);

struct Counts{
	mode_start_counts:std::collections::HashMap<ModeID,u64>,
	mode_finish_counts:std::collections::HashMap<ModeID,u64>,
	spawn_counts:std::collections::HashMap<SpawnID,u64>,
	wormhole_out_counts:std::collections::HashMap<WormholeOutID,u64>,
}
impl Counts{
	fn new()->Self{
		Self{
			mode_start_counts:std::collections::HashMap::new(),
			mode_finish_counts:std::collections::HashMap::new(),
			spawn_counts:std::collections::HashMap::new(),
			wormhole_out_counts:std::collections::HashMap::new(),
		}
	}
}

pub fn check(dom:&rbx_dom_weak::WeakDom)->CheckReport{
	// empty report with all checks failed
	let mut report=CheckReport::default();

	// extract the root instance, otherwise immediately return
	let Ok(model_instance)=get_root_instance(&dom)else{
		return report;
	};

	report.exactly_one_root=Check::Pass;

	if model_instance.class=="Model"{
		report.root_is_model=Check::Pass;
	}
	if model_instance.name==model_instance.name.to_snake_case(){
		report.model_name_is_snake_case=Check::Pass;
	}

	// extract model info
	let MapInfo{display_name,creator,game_id}=get_mapinfo(&dom,model_instance);

	// check DisplayName
	if let Ok(display_name)=display_name{
		if !display_name.is_empty(){
			report.has_display_name=Check::Pass;
			if display_name==display_name.to_title_case(){
				report.display_name_is_title_case=Check::Pass;
			}
		}
	}

	// check Creator
	if let Ok(creator)=creator{
		if !creator.is_empty(){
			report.has_creator=Check::Pass;
		}
	}

	// check GameID
	if game_id.is_ok(){
		report.model_name_prefix_is_valid=Check::Pass;
	}

	// === MODE CHECKS ===
	// count objects
	let mut counts=Counts::new();
	for instance in dom.descendants_of(model_instance.referent()){
		if class_is_a(instance.class.as_str(),"BasePart"){
			// Zones
			match instance.name.parse(){
				Ok(Zone::Start(mode_id))=>*counts.mode_start_counts.entry(mode_id).or_insert(0)+=1,
				Ok(Zone::Finish(mode_id))=>*counts.mode_finish_counts.entry(mode_id).or_insert(0)+=1,
				_=>(),
			}
			// Spawns
			let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
			if let Some(captures)=spawn_pattern.captures(instance.name.as_str()){
				if let Ok(spawn_id)=captures[1].parse(){
					*counts.spawn_counts.entry(SpawnID(spawn_id)).or_insert(0)+=1;
				}
			}
			// WormholeOuts
			let wormhole_out_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
			if let Some(captures)=wormhole_out_pattern.captures(instance.name.as_str()){
				if let Ok(wormhole_out_id)=captures[1].parse(){
					*counts.wormhole_out_counts.entry(WormholeOutID(wormhole_out_id)).or_insert(0)+=1;
				}
			}
		}
	}

	// MapStart must exist && there must be exactly one of any bonus start zones.
	if counts.mode_start_counts.get(&ModeID::MAIN)==Some(&1)
	 &&counts.mode_start_counts.iter().all(|(_,&c)|c==1){
		report.exactly_one_mapstart=Check::Pass;
	}
	// iterate over start zones
	if counts.mode_start_counts.iter().all(|(mode_id,_)|
		// ensure that at least one end zone exists with the same mode id
		counts.mode_finish_counts.get(mode_id).is_some_and(|&num|0<num)
	){
		report.at_least_one_mapfinish=Check::Pass;
	}
	// Spawn1 must exist
	if counts.spawn_counts.get(&SpawnID::FIRST).is_some(){
		report.spawn1_exists=Check::Pass;
	}
	if counts.spawn_counts.iter().all(|(_,&c)|c==1){
		report.no_duplicate_spawns=Check::Pass;
	}
	if counts.wormhole_out_counts.iter().all(|(_,&c)|c==1){
		report.no_duplicate_wormhole_out=Check::Pass;
	}

	report
}

pub struct CheckReportAndVersion{
	pub report:CheckReport,
	pub version:u64,
}

impl crate::message_handler::MessageHandler{
	pub async fn check_inner(&self,check_info:CheckRequest)->Result<CheckReportAndVersion,Error>{
		// discover asset creator and latest version
		let info=self.cloud_context.get_asset_info(
			rbx_asset::cloud::GetAssetLatestRequest{asset_id:check_info.ModelID}
		).await.map_err(Error::ModelInfoDownload)?;

		// reject models created by a group
		let rbx_asset::cloud::Creator::userId(_user_id)=info.creationContext.creator else{
			return Err(Error::CreatorTypeMustBeUser);
		};

		// parse model version string
		let version=info.revisionId;

		let maybe_gzip=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{
			asset_id:check_info.ModelID,
			version,
		}).await.map_err(Error::Download)?;

		// decode dom (slow!)
		let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;

		let report=check(&dom);

		Ok(CheckReportAndVersion{report,version})
	}
}