use proc_macro::TokenStream;
use quote::quote;
use syn::{
	Expr, Ident, LitStr, Result, Token, braced,
	parse::{Parse, ParseStream},
	parse_macro_input,
};

/// A macro for generating static asset handlers with compile-time embedding.
///
/// This macro generates:
/// - Individual structs for each asset that implement the `assetserver::Asset` trait
/// - Compile-time embedding of asset files using `include_bytes!`
/// - Optionally, an Axum router function that serves all assets
///
/// # Syntax
///
/// ```notrust
/// assets! {
///     prefix: "/assets"
///     router: router_function_name()
///
///     AssetName {
///         source: "path/to/file.ext",
///         target: "/public-url.ext"
///     }
///
///     AnotherAsset {
///         source: "path/to/another.ext",
///         target: "/another-url.ext"
///     }
/// }
/// ```
///
/// # Arguments
///
/// - `prefix`: The URL prefix for all assets (e.g., "/assets")
/// - `router`: (Optional) The name of a function to generate that returns `(&'static str, Router<()>)`
///   with routes for all assets
/// - Asset blocks: Each block defines an asset with:
///   - A name (identifier) for the generated struct
///   - `source`: The file system path to the asset (relative to the current file)
///   - `target`: The URL path where the asset will be served
///
/// # Generated Code
///
/// For each asset, the macro generates:
/// - A struct with the specified name
/// - An `assetserver::Asset` trait implementation containing:
///   - `URL_PREFIX`: The common prefix for all assets
///   - `URL`: The specific URL path for this asset
///   - `BYTES`: The embedded file contents as a byte slice
/// - Documentation showing the original asset definition
///
/// If `router` is specified, also generates a function that returns an Axum router
/// with all assets mounted at their target URLs.
///
/// # Example
///
/// ```notrust
/// assets! {
///     prefix: "/static"
///     router: static_router()
///
///     Logo {
///         source: "../images/logo.png",
///         target: "/logo.png"
///     }
/// }
/// ```
///
/// This generates structs implementing `assetserver::Asset` and optionally a router function:
///
/// ```notrust
/// pub fn static_router() -> (&'static str, ::axum::Router<()>) {
///     let router = ::axum::Router::new()
///         .route(Logo::URL, ::axum::routing::get(|| async {
///             (::axum::http::StatusCode::OK, Logo::BYTES)
///         }));
///     ("/static", router)
/// }
/// ```
#[proc_macro]
pub fn assets(input: TokenStream) -> TokenStream {
	let input = parse_macro_input!(input as AssetsInput);
	let prefix = &input.prefix;

	let asset_impls = input.assets.iter().map(|asset| {
		let name = &asset.name;
		let source = &asset.source;
		let target = &asset.target;

		// Generate documentation showing the original asset definition
		let doc = format!(
			"This is an `asset!`\n```notrust\n{} {{\n\tsource: \"{:?}\",\n\ttarget: \"{}\"\n}}\n```",
			name, source, target
		);

		quote! {
			#[expect(clippy::allow_attributes)]
			#[allow(non_camel_case_types)]
			#[doc = #doc]
			pub struct #name {}

			impl ::assetserver::Asset for #name {
				const URL_PREFIX: &'static str = #prefix;
				const URL_POSTFIX: &'static str = #target;
				const URL: &'static str = concat!(#prefix, #target);
				const BYTES: &'static [u8] = #source;
			}
		}
	});

	// Generate the router function if specified
	let router_fn = if let Some(router_name) = &input.router {
		let route_definitions = input.assets.iter().map(|asset| {
			let name = &asset.name;

			let headers = asset
				.headers
				.as_ref()
				.map(|x| quote! { #x })
				.unwrap_or(quote! { [] });

			quote! {
				.route(#name::URL_POSTFIX, ::axum::routing::get(|| async {
					(
						::axum::http::StatusCode::OK,
						#headers,
						#name::BYTES
					)
				}))
			}
		});

		let router_doc = format!(
			"Generated router function that serves {} asset(s) with prefix \"{}\"",
			input.assets.len(),
			prefix
		);

		quote! {
			#[doc = #router_doc]
			pub fn #router_name() -> (&'static str, ::axum::Router<()>) {
				use ::tower_http::compression::{CompressionLayer, DefaultPredicate};

				let compression: CompressionLayer = CompressionLayer::new()
					.br(true)
					.deflate(true)
					.gzip(true)
					.zstd(true)
					.compress_when(DefaultPredicate::new());

				let router = ::axum::Router::new()
					#(#route_definitions)*
					.layer(compression);
				(#prefix, router)
			}
		}
	} else {
		quote! {}
	};

	let expanded = quote! {
		#(#asset_impls)*

		#router_fn
	};

	TokenStream::from(expanded)
}

/// Represents the complete input to the `assets!` macro
struct AssetsInput {
	prefix: String,
	router: Option<Ident>,
	assets: Vec<AssetDefinition>,
}

/// Represents a single asset definition within the macro
struct AssetDefinition {
	name: Ident,
	source: Expr,
	target: String,
	headers: Option<Expr>,
}

impl Parse for AssetsInput {
	fn parse(input: ParseStream<'_>) -> Result<Self> {
		// Parse "prefix:"
		let _prefix_ident: Ident = input.parse()?;
		let _colon: Token![:] = input.parse()?;
		let prefix_lit: LitStr = input.parse()?;
		let prefix = prefix_lit.value();

		// Try to parse optional "router:" parameter
		let router = if input.peek(Ident) {
			let peek_ident: Ident = input.fork().parse()?;
			if peek_ident == "router" {
				let _router_ident: Ident = input.parse()?;
				let _colon: Token![:] = input.parse()?;
				let router_name: Ident = input.parse()?;
				// Parse the parentheses after the function name
				let _paren_content;
				syn::parenthesized!(_paren_content in input);
				Some(router_name)
			} else {
				None
			}
		} else {
			None
		};

		let mut assets = Vec::new();

		// Parse asset definitions until we reach the end
		while !input.is_empty() {
			let asset = input.parse::<AssetDefinition>()?;
			assets.push(asset);
		}

		Ok(AssetsInput {
			prefix,
			router,
			assets,
		})
	}
}

impl Parse for AssetDefinition {
	fn parse(input: ParseStream<'_>) -> Result<Self> {
		// Parse the asset name
		let name: Ident = input.parse()?;

		// Parse the braced content
		let content;
		braced!(content in input);

		// Parse fields in any order
		let mut source: Option<Expr> = None;
		let mut target: Option<String> = None;
		let mut headers: Option<Expr> = None;

		while !content.is_empty() {
			// Parse field name
			let field_name: Ident = content.parse()?;
			let _colon: Token![:] = content.parse()?;

			// Parse field value based on name
			match field_name.to_string().as_str() {
				"source" => {
					if source.is_some() {
						return Err(syn::Error::new(
							field_name.span(),
							"duplicate 'source' field",
						));
					}
					source = Some(content.parse()?);
				}
				"target" => {
					if target.is_some() {
						return Err(syn::Error::new(
							field_name.span(),
							"duplicate 'target' field",
						));
					}
					let target_lit: LitStr = content.parse()?;
					target = Some(target_lit.value());
				}
				"headers" => {
					if headers.is_some() {
						return Err(syn::Error::new(
							field_name.span(),
							"duplicate 'headers' field",
						));
					}
					headers = Some(content.parse()?);
				}
				_ => {
					return Err(syn::Error::new(
						field_name.span(),
						format!(
							"unknown field '{}', expected 'source', 'target', or 'headers'",
							field_name
						),
					));
				}
			}

			// Parse comma if not at end
			if !content.is_empty() {
				content.parse::<Token![,]>()?;
			}
		}

		// Validate required fields
		let source = source
			.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'source'"))?;
		let target = target
			.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'target'"))?;

		Ok(AssetDefinition {
			name,
			source,
			target,
			headers,
		})
	}
}
