//! 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::{
	Authority, NamespacedIdentifier, RecordKey, authority::AuthorityParseError,
	namespaced_identifier::NsidParseError, record_key::RecordKeyParseError,
};
use ascii::AsAsciiStr;
use serde::Deserialize;
use std::str::FromStr;

/// See https://atproto.com/specs/at-uri-scheme
#[derive(Debug, PartialEq, Eq)]
pub struct AtUri {
	authority: Authority,
	collection: Option<NamespacedIdentifier>, // Spec: "Every collection is identified by an NSID which is expected to map to a Lexicon schema." (https://atproto.com/guides/glossary#collection)
	record_key: Option<RecordKey>,
}

impl FromStr for AtUri {
	type Err = AtUriParseError;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		// Full AT URI Syntax: https://atproto.com/specs/at-uri-scheme#full-at-uri-syntax

		// Spec: "The URI scheme is `at`, and an authority part preceded with double slashes is always required, so the URI always starts `at://`"
		let s = s
			.strip_prefix("at://")
			.ok_or(AtUriParseError::MissingOrInvalidScheme)?;

		// Spec: "The overall URI is restricted to a subset of ASCII characters"
		let s = s.as_ascii_str()?;

		// Spec: "Maximum overall length is 8 kilobytes (which may be shortened in the future)"
		if s.len() > 8_192 {
			return Err(AtUriParseError::TooLong);
		}

		// Spec: "An optional path section may follow the authority. The path may contain multiple segments separated by a single slash (/). Generic URI path normalization rules may be used."
		let mut segments = s.split(ascii::AsciiChar::Slash);

		// Spec: "An authority section is required and must be non-empty. The authority can be either an atproto Handle, or a DID meeting the restrictions for use with atproto."
		let authority = segments.next().expect("nonempty");
		let authority = Authority::from_str(authority.as_str())?;

		// TODO: Spec: "Generic URI path normalization rules may be used." See https://atproto.com/specs/at-uri-scheme#normalization

		// Restricted AT URI Syntax: https://atproto.com/specs/at-uri-scheme#restricted-at-uri-syntax

		let collection = match segments.next() {
			None => None,
			Some(collection) => {
				let nsid = NamespacedIdentifier::from_str(collection.as_str())?;
				if nsid.fragment().is_some() {
					return Err(AtUriParseError::IllegalFragment);
				}
				Some(nsid)
			}
		};

		let record_key = match segments.next() {
			None => None,
			Some(rkey) => Some(RecordKey::from_str(rkey.as_str())?),
		};

		Ok(Self {
			authority,
			collection,
			record_key,
		})
	}
}

impl<'de> Deserialize<'de> for AtUri {
	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 core::fmt::Display for AtUri {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		let authority = &self.authority;
		let collection = &self.collection;
		let record_key = &self.record_key;

		// Restricted AT URI Syntax: https://atproto.com/specs/at-uri-scheme#restricted-at-uri-syntax
		// Spec: "Trailing slashes are not allowed, including a trailing slash after the authority with no other path."
		// TODO: Spec: "The URI should be in normalized form (see "Normalization" section), with all of the individual sub-identifiers also normalized."
		match collection {
			None => write!(f, "at://{authority}"),
			Some(collection) => match record_key {
				None => write!(f, "at://{authority}/{collection}"),
				Some(record_key) => write!(f, "at://{authority}/{collection}/{record_key}"),
			},
		}
	}
}

impl AtUri {
	pub fn from_post(authority: Authority, record_key: RecordKey) -> Self {
		Self {
			authority,
			collection: Some(NamespacedIdentifier::app_bsky_feed_post()),
			record_key: Some(record_key),
		}
	}

	/// Returns a path string that may be used to navigate browsers to this host's
	/// representation of the AT URI's target.
	pub fn to_local_uri(&self) -> String {
		match &self.record_key {
			None => {
				format!("/profile/{}", self.authority)
			}
			Some(rkey) => format!("/profile/{}/post/{rkey}", self.authority),
		}
	}
}

#[derive(Debug)]
pub enum AtUriParseError {
	/// The Collection segment contains a fragment portion (`#`).
	IllegalFragment,

	/// Value does not contain a valid Authority.
	InvalidAuthority(AuthorityParseError),

	/// Value's Collection segment is invalid.
	InvalidCollection(NsidParseError),

	/// Value's Record Key segment is invalid.
	InvalidRecordKey(RecordKeyParseError),

	/// Value does not begin with `at://`.
	MissingOrInvalidScheme,

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

	/// Value is more than 8 kilobytes in length.
	TooLong,
}

impl core::fmt::Display for AtUriParseError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::IllegalFragment => write!(f, "AT URI value's Collection contains a fragment"),
			Self::InvalidAuthority(err) => {
				write!(f, "AT URI value does not contain a valid Authority: {err}")
			}
			Self::InvalidCollection(err) => {
				write!(f, "AT URI value contains an invalid Collection: {err}")
			}
			Self::InvalidRecordKey(err) => {
				write!(f, "AT URI value contains an invalid Record Key: {err}")
			}
			Self::MissingOrInvalidScheme => write!(f, "AT URI value does not begin with `at://`"),
			Self::NotAscii => write!(f, "AT URI value contains non-ASCII characters"),
			Self::TooLong => write!(f, "AT URI value is more than 8 kilobytes in length"),
		}
	}
}

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

impl From<AuthorityParseError> for AtUriParseError {
	fn from(value: AuthorityParseError) -> Self {
		Self::InvalidAuthority(value)
	}
}

impl From<NsidParseError> for AtUriParseError {
	fn from(value: NsidParseError) -> Self {
		Self::InvalidCollection(value)
	}
}

impl From<RecordKeyParseError> for AtUriParseError {
	fn from(value: RecordKeyParseError) -> Self {
		Self::InvalidRecordKey(value)
	}
}

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

// MARK: - Tests

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

	#[test]
	fn test_parsing() {
		let result: AtUri = "at://foo.com/com.example.foo/123".parse().unwrap();
		let expected = AtUri {
			authority: Authority::Handle("foo.com".parse().unwrap()),
			collection: Some("com.example.foo".parse().unwrap()),
			record_key: Some(RecordKey::Any("123".as_ascii_str().unwrap().to_owned())),
		};
		assert_eq!(result, expected);
		assert_eq!(result.to_string(), expected.to_string());
	}

	#[test]
	fn test_parse_lexicon_failure() {
		let err1 = "at://foo.com/example/123".parse::<AtUri>().err().unwrap();
		assert!(matches!(err1, AtUriParseError::InvalidCollection(_)));

		let err2 = "at://computer".parse::<AtUri>().err().unwrap();
		assert!(matches!(err2, AtUriParseError::InvalidAuthority(_)));

		let err3 = "at://example.com:3000".parse::<AtUri>().err().unwrap();
		assert!(matches!(err3, AtUriParseError::InvalidAuthority(_)));
	}

	#[test]
	fn test_parse_syntax_failure() {
		let err1 = "at://foo.com/".parse::<AtUri>().err().unwrap();
		assert!(matches!(
			err1,
			AtUriParseError::InvalidCollection(NsidParseError::NotEnoughSegments)
		));

		let err2 = "at://user:pass@foo.com".parse::<AtUri>().err().unwrap();
		assert!(matches!(err2, AtUriParseError::InvalidAuthority(_)));
	}
}
