//! 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 ascii::AsAsciiStr;
use serde::Deserialize;
use std::str::FromStr;

/// See https://atproto.com/specs/handle
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Handle(ascii::AsciiString); // Spec: "The overall handle must contain only ASCII characters"

impl FromStr for Handle {
	type Err = HandleParseError;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		// Handle Identifier Syntax: https://atproto.com/specs/handle#handle-identifier-syntax

		// Spec: "No proceeding or trailing ASCII periods are allowed"
		const DOT: char = '.';
		if s.starts_with(DOT) || s.ends_with(DOT) {
			return Err(HandleParseError::IllegalDot);
		}

		// Spec: "Handles are not case-sensitive, and should be normalized to lowercase (that is, normalize ASCII A-Z to a-z)"
		let lower = s.to_ascii_lowercase();

		// Spec: "The overall handle must contain only ASCII characters, and can be at most 253 characters long"
		let s = lower.as_ascii_str()?;
		if s.len() > 253 {
			return Err(HandleParseError::TooLong);
		}

		let segments: Vec<_> = s.split(ascii::AsciiChar::Dot).collect(); // doesn't include dots

		// Spec: "there must be at least two segments"
		if segments.len() < 2 {
			return Err(HandleParseError::NotEnoughSegments);
		}

		// Spec: "The last segment (the "top level domain") can not start with a numeric digit"
		let tld = *segments.last().expect("nonempty");
		match tld.first() {
			None => return Err(HandleParseError::EmptySegment),
			Some(start) => {
				if start.is_ascii_digit() {
					return Err(HandleParseError::IllegalDigit);
				}
			}
		}

		for segment in segments {
			// Spec: "Each segment must have at least 1 and at most 63 characters"
			if segment.is_empty() {
				return Err(HandleParseError::EmptySegment);
			} else if segment.len() > 63 {
				return Err(HandleParseError::SegmentTooLong);
			}

			// Spec: "Segments can not start or end with a hyphen"
			const HYPHEN: ascii::AsciiChar = ascii::AsciiChar::Minus;
			if segment.first().expect("nonempty") == HYPHEN
				|| segment.last().expect("nonempty") == HYPHEN
			{
				return Err(HandleParseError::IllegalHyphen);
			}

			// Spec: "The allowed characters are ASCII letters (a-z), digits (0-9), and hyphens (-)."
			for char in segment {
				if !char.is_alphanumeric() && char != &HYPHEN {
					return Err(HandleParseError::IllegalSegmentCharacter);
				}
			}
		}

		// Additional Non-Syntax Restrictions: https://atproto.com/specs/handle#additional-non-syntax-restrictions
		// Spec: "'Reserved' top-level domains should not fail syntax validation (eg, in atproto Lexicon validation), but they must immediately fail any attempt at registration, resolution, etc."
		static RESERVED_TLDS: &[&str] = &[
			// Spec: "the initial list of disallowed TLDs includes:"
			"alt",
			"arpa",
			"example",
			"internal",
			"invalid", // Spec: "The `.invalid` TLD should only be used for the special `handle.invalid` value (see below). This value is syntactically valid in the Lexicon schema language, but should not be accepted as a valid handle in most contexts."
			"local", // Spec: "`.local` hostnames (for mDNS on local networks) should not be used in atproto."
			"localhost",
			"onion", // Spec: "The `.onion` TLD is a special case for Tor protocol hidden services. Resolution of handles via Tor would require ecosystem-wide support, so they are currently disallowed."
		];
		let mut reserved_tlds = RESERVED_TLDS
			.iter()
			.map(|tld| tld.as_ascii_str().expect("ascii"));
		if reserved_tlds.any(|t| t == tld) {
			return Err(HandleParseError::IllegalTld);
		}

		// Spec: "The `.test` TLD is intended for examples, testing, and development. It may be used in atproto development, but should fail in real-world environments."
		#[cfg(not(debug_assertions))]
		if tld == "test" {
			return Err(HandleParseError::IllegalTld);
		}

		Ok(Self(s.to_owned()))
	}
}

impl core::fmt::Display for Handle {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.0)
	}
}

impl<'de> Deserialize<'de> for Handle {
	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		let s = String::deserialize(deserializer)?;
		match Self::from_str(&s) {
			Ok(at_uri) => Ok(at_uri),
			Err(err) => Err(serde::de::Error::custom(err.to_string())),
		}
	}
}

impl Handle {
	// /// The special `handle.invalid` handle.
	// ///
	// /// From the [spec]:
	// /// <blockquote>
	// /// If the handle for a known DID is confirmed to no longer resolve, it should be
	// /// marked as invalid. In API responses, the special handle value `handle.invalid`
	// /// can be used to indicate that there is no bi-directionally valid handle for the
	// /// given DID. This handle can not be used in most situations (search queries, API
	// /// requests, etc).
	// /// </blockquote>
	// ///
	// /// [spec]: https://atproto.com/specs/handle#invalid-handles
	// pub fn invalid() -> Self {
	// 	Handle("handle.invalid".as_ascii_str().expect("ascii").to_owned())
	// }

	/// Returns an iterator over segments of this handle.
	pub fn segments(&self) -> impl DoubleEndedIterator<Item = &ascii::AsciiStr> {
		self.0.split(ascii::AsciiChar::Dot)
	}
}

#[derive(Debug)]
pub enum HandleParseError {
	/// A segment is empty.
	EmptySegment,

	/// The last segment begins with a numeric digit.
	IllegalDigit,

	/// Value starts or ends with an ASCII dot (`.`).
	IllegalDot,

	/// A segment begins or ends with a hyphen (`-`).
	IllegalHyphen,

	/// A segment contains one or more characters that aren't alphanumeric or a hyphen (`-`).
	IllegalSegmentCharacter,

	/// The final segment (the "top level domain") is on a list of reserved TLDs that should not be used in atproto.
	IllegalTld,

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

	/// Value contains fewer than 2 segments.
	NotEnoughSegments,

	/// A segment is more than 63 characters in length.
	SegmentTooLong,

	/// Value is more than 253 characters in length.
	TooLong,
}

impl core::fmt::Display for HandleParseError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::EmptySegment => write!(f, "a segment is empty"),
			Self::IllegalDigit => write!(
				f,
				"a segment contains one or more characters that aren't alphanumeric or a hyphen (`-`)"
			),
			Self::IllegalDot => write!(f, "value starts or ends with an ASCII dot (`.`)"),
			Self::IllegalHyphen => write!(f, "a segment begins or ends with a hyphen (`-`)"),
			Self::IllegalSegmentCharacter => write!(
				f,
				"a segment contains one or more characters that aren't alphanumeric or a hyphen (`-`)"
			),
			Self::IllegalTld => write!(
				f,
				r#"the final segment (the "top level domain") is on a list of reserved TLDs that should not be used in atproto"#
			),
			Self::NotAscii => write!(f, "value contains non-ASCII characters"),
			Self::NotEnoughSegments => write!(f, "value contains are fewer than 2 segments"),
			Self::SegmentTooLong => write!(f, "a segment is more than 63 characters in length"),
			Self::TooLong => write!(f, "value is more than 253 characters in length"),
		}
	}
}

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

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

// MARK: - Tests

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

	#[test]
	fn test_parsing() {
		// See https://atproto.com/specs/handle#identifier-examples
		let cases = [
			// Spec: "Syntactically valid handles (which may or may not have existing TLDs):"
			// TODO: Test expected inner data
			"jay.bsky.social",
			"8.cn",
			"name.t--t", // not a real TLD, but syntax ok
			"XX.LCS.MIT.EDU",
			"a.co",
			"xn--notarealidn.com",
			"xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s",
			#[cfg(debug_assertions)]
			"xn--ls8h.test",
			#[cfg(not(debug_assertions))]
			"xn--ls8h.com",
			"example.t", // not a real TLD, but syntax ok
		];

		for case in cases {
			let expected = Handle(case.to_ascii_lowercase().as_ascii_str().unwrap().to_owned());
			assert_eq!(case.parse::<Handle>().unwrap(), expected);
		}
	}

	#[test]
	fn test_invalid_syntax() {
		// See https://atproto.com/specs/handle#identifier-examples
		let cases = [
			// Spec: "Invalid syntax:"
			#[cfg(debug_assertions)]
			"jo@hn.test",
			#[cfg(not(debug_assertions))]
			"jo@hn.com",
			#[cfg(debug_assertions)]
			"💩.test",
			#[cfg(not(debug_assertions))]
			"💩.com",
			#[cfg(debug_assertions)]
			"john..test",
			#[cfg(not(debug_assertions))]
			"john..com",
			"xn--bcher-.tld",
			"john.0",
			"cn.8",
			"www.masełkowski.pl.com",
			"org",
			"name.org.",
			// Spec: "Valid syntax, but must always fail resolution due to other restrictions:"
			"2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
			"laptop.local",
			"blah.arpa",
		];

		for case in cases {
			assert!(case.parse::<Handle>().is_err()); // TODO: Assert specific errors
		}
	}
}
