//! atproto-static-web   A simple web viewer for AT Proto.
//! Copyright (C) 2025  AverageHelper
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::atproto::{NamespacedIdentifier, TimestampIdentifier};
use ascii::AsAsciiStr;
use std::str::FromStr;

/// See https://atproto.com/specs/record-key
#[derive(Debug, PartialEq, Eq)]
pub enum RecordKey {
	/// Record Key Type: [`tid`]
	///
	/// [`tid`]: https://atproto.com/specs/record-key#record-key-type-tid
	TimestampIdentifier(TimestampIdentifier),

	/// Record Key Type: [`nsid`]
	///
	///  [`nsid`]: https://atproto.com/specs/record-key#record-key-type-nsid
	NamespacedIdentifier(NamespacedIdentifier),

	// TODO
	// /// Record Key Type: [`literal:<value>`]
	// ///
	// /// [`literal:<value>`]: https://atproto.com/specs/record-key#record-key-type-literal-value
	// LiteralSelf, // "literal:self"
	//
	/// Record Key Type: [`any`]
	///
	/// [`any`]: https://atproto.com/specs/record-key#record-key-type-any
	Any(ascii::AsciiString), // Spec: "restricted to a subset of ASCII characters"
}

impl core::fmt::Display for RecordKey {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::TimestampIdentifier(tid) => write!(f, "{tid}"),
			Self::NamespacedIdentifier(nsid) => write!(f, "{nsid}"),
			Self::Any(value) => write!(f, "{value}"),
		}
	}
}

impl FromStr for RecordKey {
	type Err = RecordKeyParseError;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		// Record Key Syntax: https://atproto.com/specs/record-key#record-key-syntax

		// Spec: "must have at least 1 and at most 512 characters"
		if s.is_empty() {
			return Err(RecordKeyParseError::Empty);
		}
		if s.len() > 512 {
			return Err(RecordKeyParseError::TooLong);
		}

		// Spec: "the specific record key values . and .. are not allowed"
		if s == "." || s == ".." {
			return Err(RecordKeyParseError::IllegalValue);
		}

		// Spec: "restricted to a subset of ASCII characters — the allowed characters are alphanumeric (A-Za-z0-9), period, dash, underscore, colon, or tilde (.-_:~)"
		let s = s.as_ascii_str()?;
		for char in s {
			if !char.is_alphanumeric()
				&& *char != '.'
				&& *char != '-'
				&& *char != '_'
				&& *char != ':'
				&& *char != '~'
			{
				return Err(RecordKeyParseError::IllegalCharacter);
			}
		}

		match TimestampIdentifier::from_ascii_str(s) {
			Ok(tid) => Ok(Self::TimestampIdentifier(tid)),
			Err(_) => match NamespacedIdentifier::from_ascii_str(s) {
				Ok(nsid) => Ok(Self::NamespacedIdentifier(nsid)),
				Err(_) => Ok(Self::Any(s.to_owned())),
			},
		}
	}
}

impl From<TimestampIdentifier> for RecordKey {
	fn from(value: TimestampIdentifier) -> Self {
		Self::TimestampIdentifier(value)
	}
}

impl From<NamespacedIdentifier> for RecordKey {
	fn from(value: NamespacedIdentifier) -> Self {
		Self::NamespacedIdentifier(value)
	}
}

#[derive(Debug)]
pub enum RecordKeyParseError {
	/// The value is empty.
	Empty,

	/// Value contains an invalid character.
	IllegalCharacter,

	/// Value is either '.' or '..'
	IllegalValue,

	/// Value contains non-ASCII characters.
	NotAscii,

	/// The value is more than 512 characters in length.
	TooLong,
}

impl core::fmt::Display for RecordKeyParseError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Empty => write!(f, "the record key is empty"),
			Self::IllegalCharacter => write!(f, "record key contains an invalid character"),
			Self::IllegalValue => write!(f, "record key is either '.' or '..'"),
			Self::NotAscii => write!(f, "record key contains non-ASCII characters"),
			Self::TooLong => write!(f, "record key is more than 512 characters in length"),
		}
	}
}

impl core::error::Error for RecordKeyParseError {}

impl From<ascii::AsAsciiStrError> for RecordKeyParseError {
	fn from(_: ascii::AsAsciiStrError) -> Self {
		Self::NotAscii
	}
}

// MARK: - Tests

#[cfg(test)]
mod tests {
	use super::*;

	macro_rules! ascii {
		($l:literal) => {
			$l.as_ascii_str().unwrap().to_owned()
		};
	}

	#[test]
	fn test_parsing() {
		let cases = [
			// Canonical:
			(
				"3jui7kd54zh2y",
				RecordKey::TimestampIdentifier("3jui7kd54zh2y".parse().unwrap()),
			),
			("self", RecordKey::Any(ascii!("self"))),
			("example.com", RecordKey::Any(ascii!("example.com"))),
			("~1.2-3_", RecordKey::Any(ascii!("~1.2-3_"))),
			("dHJ1ZQ", RecordKey::Any(ascii!("dHJ1ZQ"))),
			("pre:fix", RecordKey::Any(ascii!("pre:fix"))),
			("_", RecordKey::Any(ascii!("_"))),
			// Extra:
			(
				"com.example.fooBar",
				RecordKey::NamespacedIdentifier("com.example.fooBar".parse().unwrap()),
			),
		];
		for (key, expected) in cases {
			let result = key.parse::<RecordKey>().unwrap();
			assert_eq!(result, expected);
		}
	}

	#[test]
	fn test_invalid_syntax() {
		let cases = [
			"alpha/beta",
			".",
			"..",
			"#extra",
			"@handle",
			"any space",
			"any+space",
			"number[3]",
			"number(3)",
			r#""quote""#,
			"dHJ1ZQ==",
		];
		for case in cases {
			let result = case.parse::<RecordKey>();
			assert!(result.is_err()); // TODO: Assert specific errors
		}
	}
}
