// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use std::collections::HashMap;

use super::super::ThemeError;

/// Read the top-level `colors` object from the raw JSON value and validate
/// each entry is a literal hex string. Returns an empty map if no `colors`
/// section is present.
pub( super ) fn extract_colors_map( value: &serde_json::Value )
	-> Result<HashMap<String, String>, ThemeError>
{
	let mut out = HashMap::new();
	let raw = match value.get( "colors" )
	{
		Some( c ) => c,
		None      => return Ok( out ),
	};
	let map = raw.as_object().ok_or_else( ||
		ThemeError::InvalidColor( "`colors` must be an object".to_string() )
	)?;
	for ( k, v ) in map
	{
		let s = v.as_str().ok_or_else( ||
			ThemeError::InvalidColor( format!( "`colors.{}` must be a hex string", k ) )
		)?;
		// References inside `colors` itself are not supported — every entry
		// must be a literal hex value the rest of the document can point at.
		let h = s.trim_start_matches( '#' );
		let valid = ( h.len() == 6 || h.len() == 8 ) && h.chars().all( |c| c.is_ascii_hexdigit() );
		if !valid
		{
			return Err( ThemeError::InvalidColor(
				format!( "`colors.{}` = `{}` (expected #RRGGBB or #RRGGBBAA)", k, s )
			));
		}
		out.insert( k.clone(), s.to_string() );
	}
	Ok( out )
}

/// Read the top-level `gradients` object from the raw JSON value. Each
/// entry must be a paint object (`{ "type": "linear", "angle_deg": …,
/// "stops": [ … ] }`); shape correctness beyond "is an object" is left
/// for downstream serde deserialisation to enforce when the gradient is
/// substituted into a paint position.
pub( super ) fn extract_gradients_map( value: &serde_json::Value )
	-> Result<HashMap<String, serde_json::Value>, ThemeError>
{
	extract_token_map( value, "gradients", "paint object", serde_json::Value::is_object )
}

/// Read the top-level `inset_stacks` object from the raw JSON value. Each
/// entry must be an array of inset-shadow definitions — substituted
/// wholesale into an `inset_shadows` field on a surface.
pub( super ) fn extract_inset_stacks_map( value: &serde_json::Value )
	-> Result<HashMap<String, serde_json::Value>, ThemeError>
{
	extract_token_map( value, "inset_stacks", "array of inset-shadow definitions", serde_json::Value::is_array )
}

/// Generic helper for the `gradients` / `inset_stacks` extractors above:
/// reads a top-level object, validates each entry against `valid`, and
/// returns the entries cloned into a `HashMap`. Returns an empty map if
/// `section` is absent.
fn extract_token_map(
	value:         &serde_json::Value,
	section:       &str,
	expected_kind: &str,
	valid:         impl Fn( &serde_json::Value ) -> bool,
) -> Result<HashMap<String, serde_json::Value>, ThemeError>
{
	let mut out = HashMap::new();
	let raw = match value.get( section )
	{
		Some( c ) => c,
		None      => return Ok( out ),
	};
	let map = raw.as_object().ok_or_else( ||
		ThemeError::InvalidColor( format!( "`{}` must be an object", section ) )
	)?;
	for ( k, v ) in map
	{
		if !valid( v )
		{
			return Err( ThemeError::InvalidColor(
				format!( "`{}.{}` must be {}", section, k, expected_kind )
			));
		}
		out.insert( k.clone(), v.clone() );
	}
	Ok( out )
}

/// Walk `value` and replace `@name` / `@name/AA` references with their
/// resolved form. A reference whose name appears in `tokens` (gradient
/// objects or inset-shadow arrays) is substituted by the cloned token
/// value; a reference whose name appears in `colors` is substituted by
/// its hex literal. After a substitution the new node is recursed into
/// so a gradient that uses `@cyan-soft` in its stops or an inset stack
/// that uses `@glass-hi` is fully expanded.
pub( super ) fn resolve_refs(
	value:  &mut serde_json::Value,
	colors: &HashMap<String, String>,
	tokens: &HashMap<String, serde_json::Value>,
) -> Result<(), ThemeError>
{
	// Step 1: if this node is a `@ref` string, resolve and overwrite.
	let replacement = match value
	{
		serde_json::Value::String( s ) => match s.strip_prefix( '@' )
		{
			Some( rest ) => Some( resolve_one_ref( rest, colors, tokens )? ),
			None         => None,
		},
		_ => None,
	};
	if let Some( new_value ) = replacement
	{
		*value = new_value;
		// The replacement may itself contain references (typically a
		// gradient with `@color` stops). Recurse into the new node.
		resolve_refs( value, colors, tokens )?;
		return Ok( () );
	}
	// Step 2: walk children of objects / arrays.
	match value
	{
		serde_json::Value::Object( map ) =>
		{
			for ( _, v ) in map.iter_mut()
			{
				resolve_refs( v, colors, tokens )?;
			}
		}
		serde_json::Value::Array( arr ) =>
		{
			for v in arr.iter_mut()
			{
				resolve_refs( v, colors, tokens )?;
			}
		}
		_ => {}
	}
	Ok( () )
}

/// Resolve a single `@name[/AA]` reference. Looks up `tokens` first
/// (gradient or inset-stack), then `colors`. The `/AA` alpha override
/// only applies to colour references; using it on a non-colour token is
/// rejected.
fn resolve_one_ref(
	rest:   &str,
	colors: &HashMap<String, String>,
	tokens: &HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value, ThemeError>
{
	let ( name, alpha_hex ) = match rest.split_once( '/' )
	{
		Some( ( n, a ) ) => ( n, Some( a ) ),
		None             => ( rest, None ),
	};
	if let Some( tok ) = tokens.get( name )
	{
		if let Some( a ) = alpha_hex
		{
			return Err( ThemeError::InvalidColor( format!(
				"@{} is a paint/inset token — alpha override `/{}` is not applicable", name, a
			)));
		}
		return Ok( tok.clone() );
	}
	let base = colors.get( name )
		.ok_or_else( || ThemeError::UnknownColorRef( name.to_string() ) )?;
	let h    = base.trim_start_matches( '#' );
	// `extract_colors_map` already validated the base is 6 or 8 hex digits.
	let rgb  = &h[0..6];
	let s = match alpha_hex
	{
		Some( a ) =>
		{
			if a.len() != 2 || u8::from_str_radix( a, 16 ).is_err()
			{
				return Err( ThemeError::InvalidColor(
					format!( "@{}/{} (alpha must be two hex digits)", name, a )
				));
			}
			format!( "#{}{}", rgb.to_uppercase(), a.to_uppercase() )
		}
		None => format!( "#{}", h.to_uppercase() ),
	};
	Ok( serde_json::Value::String( s ) )
}
