//! 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::{collections::VecDeque, str::FromStr};

/// See https://atproto.com/specs/did
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DecentralizedIdentifier {
	method: Method,
	identifier: ascii::AsciiString,
}

impl<'de> Deserialize<'de> for DecentralizedIdentifier {
	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(did) => Ok(did),
			Err(err) => Err(serde::de::Error::custom(err.to_string())),
		}
	}
}

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

/// See https://atproto.com/specs/did#blessed-did-methods
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Method {
	// Spec: "Currently, atproto supports two DID methods:"
	Web, // "did:web"
	Plc, // "did:plc"
}

impl core::fmt::Display for Method {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Web => write!(f, "did:web"),
			Self::Plc => write!(f, "did:plc"),
		}
	}
}

impl FromStr for DecentralizedIdentifier {
	type Err = DidParseError;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		// AT Protocol DID Identifier Syntax: https://atproto.com/specs/did#at-protocol-did-identifier-syntax

		// Soec: "DID identifiers do not generally have a maximum length restriction, but in the context of atproto, there is an initial hard limit of 2048 characters."
		if s.len() > 2048 {
			return Err(DidParseError::TooLong);
		}

		// Spec: "The URI (and thus the remaining identifier) may not end in `:`."
		if s.ends_with(":") {
			return Err(DidParseError::InvalidSuffix);
		}

		let mut segments: VecDeque<_> = s.split(':').collect();

		// Spec: "The URI starts with lowercase `did:`"
		let prefix = segments.pop_front().expect("first");
		if prefix != "did" {
			return Err(DidParseError::InvalidPrefix);
		}

		// Spec: "The method segment is one or more lowercase letters (`a-z`), followed by `:`"
		let method = match segments
			.pop_front()
			.ok_or(DidParseError::NotEnoughSegments)?
		{
			"plc" => Method::Plc,
			"web" => Method::Web,
			_ => return Err(DidParseError::UnsupportedMethod),
		};

		// Spec: "The entire URI is made up of a subset of ASCII, containing letters (`A-Z`, `a-z`), digits (`0-9`), period, underscore, colon, percent sign, or hyphen (`._:%-`)"
		// Spec: "The remainder of the URI (the identifier) may contain any of the above-allowed ASCII characters, except for percent-sign (`%`)"
		// We've already checked up to the Method segment, so here we only convert the remainder:
		let identifier = segments.make_contiguous().join(":");
		let identifier = identifier.as_ascii_str()?;

		for char in identifier {
			if !char.is_alphanumeric()
				&& *char != '.'
				&& *char != '_'
				&& *char != ':'
				&& *char != '-'
			{
				return Err(DidParseError::InvalidIdentifier);
			}
		}

		// Spec: "Query (`?`) and fragment (`#`) sections are allowed in DID URIs, but not in DID identifiers. In the context of atproto, the query and fragment parts are not allowed."
		if s.contains("?") || s.contains("#") {
			return Err(DidParseError::InvalidIdentifier); // TODO: Should this be a different error?
		}

		// TODO: Spec: "Percent-sign (`%`) is used for "percent encoding" in the identifier section, and must always be followed by two hex characters"
		// What does this even mean? I thought % was illegal in the identifier???

		// TODO: Spec: "A DID containing invalid percent encoding should fail any attempt at registration, resolution, etc."

		Ok(Self {
			method,
			identifier: identifier.to_owned(),
		})
	}
}

impl DecentralizedIdentifier {
	fn as_str(&self) -> String {
		format!("{}:{}", self.method, self.identifier)
	}
}

#[derive(Debug)]
pub enum DidParseError {
	/// Value contains an invalid character.
	InvalidIdentifier,

	/// Value does not start with lowercase `did:`.
	InvalidPrefix,

	/// Value ends with `:`.
	InvalidSuffix,

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

	/// Value does not contain enough segments.
	NotEnoughSegments,

	/// Value exceeds 2048 characters in length.
	TooLong,

	/// The value's `method` segment is not supported.
	UnsupportedMethod,
}

impl core::fmt::Display for DidParseError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::InvalidIdentifier => write!(f, "DID value contains an invalid character"),
			Self::InvalidPrefix => write!(f, "DID value does not start with lowercase `did:`"),
			Self::InvalidSuffix => write!(f, "DID value ends with `:`"),
			Self::NotAscii => write!(f, "DID value contains non-ASCII characters"),
			Self::NotEnoughSegments => write!(f, "DID value does not contain enough segments"),
			Self::TooLong => write!(f, "DID value exceeds 2048 characters in length"),
			Self::UnsupportedMethod => {
				write!(f, "the DID value's `method` segment is not supported")
			}
		}
	}
}

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

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

// MARK: - Tests

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

	#[test]
	fn test_parsing() {
		// See https://atproto.com/specs/did#examples
		// Spec: "Valid DIDs for use in atproto (correct syntax, and supported method):"
		let result1 = "did:plc:z72i7hdynmk6r22z27h6tvur"
			.parse::<DecentralizedIdentifier>()
			.unwrap();
		assert_eq!(result1.method, Method::Plc);
		assert_eq!(result1.identifier.as_str(), "z72i7hdynmk6r22z27h6tvur");

		let result2 = "did:web:blueskyweb.xyz"
			.parse::<DecentralizedIdentifier>()
			.unwrap();
		assert_eq!(result2.method, Method::Web);
		assert_eq!(result2.identifier.as_str(), "blueskyweb.xyz");
	}

	#[test]
	fn test_unsupported_methods() {
		// See https://atproto.com/specs/did#examples
		// Spec: "Valid DID syntax (would pass Lexicon syntax validation), but unsupported DID method:"
		let cases = [
			"did:method:val:two",
			"did:m:v",
			"did:method::::val",
			"did:method:-:_:.",
			"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
		];
		for case in cases {
			let err = case.parse::<DecentralizedIdentifier>().err().unwrap();
			assert!(matches!(err, DidParseError::UnsupportedMethod));
		}
	}

	#[test]
	fn test_invalid_syntax() {
		// See https://atproto.com/specs/did#examples
		// Spec: "Invalid DID identifier syntax (regardless of DID method):"
		let cases = [
			"did:method:val:two",
			"did:m:v",
			"did:method::::val",
			"did:method:-:_:.",
			"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
		];
		for case in cases {
			let result = case.parse::<DecentralizedIdentifier>();
			assert!(result.is_err()); // TODO: Assert specific errors
		}
	}
}
