use std::{collections::BTreeSet, fmt};

use semver::Version;
use serde::Deserialize;
use url::Url;

use crate::DomainError;

mod signature;

use signature::RawPluginSignature;
pub use signature::{PluginSignature, PluginSignatureAlgorithm};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PluginId(String);

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PluginManifest {
    id: PluginId,
    name: String,
    description: String,
    author: String,
    homepage: String,
    permissions: Vec<PluginPermission>,
    contributes: Vec<PluginContributionPoint>,
    min_ely_build: Version,
    checksum: String,
    signature: PluginSignature,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PluginPermission {
    TabsRead,
    TabsWrite,
    SpacesRead,
    SpacesWrite,
    BookmarksRead,
    BookmarksWrite,
    HistoryRead,
    DownloadsRead,
    DownloadsWrite,
    PageMetadata,
    PageScreenshot,
    PageScript,
    ClipboardRead,
    ClipboardWrite,
    FilesystemRead,
    FilesystemWrite,
    NetworkFetch,
    SettingsRead,
    SettingsWrite,
    SyncPlugin,
    UiPanel,
    UiCommand,
    UiContextMenu,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PluginPermissionRisk {
    Standard,
    High,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PluginContributionPoint {
    CommandBarCommand,
    TabContextMenu,
    PageContextMenu,
    SidebarPanel,
    SettingsPage,
    StatusBarIndicator,
    DownloadAction,
    BookmarkAction,
    ReadingModeExporter,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPluginManifest {
    id: String,
    name: String,
    description: String,
    author: String,
    homepage: String,
    permissions: Vec<String>,
    contributes: Vec<String>,
    min_ely_build: String,
    checksum: String,
    signature: RawPluginSignature,
}

impl PluginId {
    pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
        let value = value.into();
        let value = value.trim();
        if !is_valid_plugin_id(value) {
            return Err(DomainError::InvalidPluginId { value: value.to_string() });
        }

        Ok(Self(value.to_string()))
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for PluginId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl PluginManifest {
    pub fn from_toml(value: &str) -> Result<Self, DomainError> {
        let raw: RawPluginManifest = toml::from_str(value)
            .map_err(|error| DomainError::InvalidPluginManifest { reason: error.to_string() })?;

        Self::from_raw(raw)
    }

    fn from_raw(raw: RawPluginManifest) -> Result<Self, DomainError> {
        Ok(Self {
            id: PluginId::parse(raw.id)?,
            name: non_empty_plugin_field("name", raw.name)?,
            description: non_empty_plugin_field("description", raw.description)?,
            author: non_empty_plugin_field("author", raw.author)?,
            homepage: plugin_homepage(raw.homepage)?,
            permissions: parse_unique_permissions(&raw.permissions)?,
            contributes: parse_unique_contributions(&raw.contributes)?,
            min_ely_build: parse_ely_build(raw.min_ely_build)?,
            checksum: plugin_checksum(raw.checksum)?,
            signature: PluginSignature::from_raw(raw.signature)?,
        })
    }

    #[must_use]
    pub fn id(&self) -> &PluginId {
        &self.id
    }

    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    #[must_use]
    pub fn description(&self) -> &str {
        &self.description
    }

    #[must_use]
    pub fn author(&self) -> &str {
        &self.author
    }

    #[must_use]
    pub fn homepage(&self) -> &str {
        &self.homepage
    }

    #[must_use]
    pub fn permissions(&self) -> &[PluginPermission] {
        &self.permissions
    }

    pub fn high_risk_permissions(&self) -> impl Iterator<Item = &PluginPermission> {
        self.permissions.iter().filter(|permission| permission.requires_separate_confirmation())
    }

    #[must_use]
    pub fn contributes(&self) -> &[PluginContributionPoint] {
        &self.contributes
    }

    #[must_use]
    pub fn min_ely_build(&self) -> &Version {
        &self.min_ely_build
    }

    #[must_use]
    pub fn checksum(&self) -> &str {
        &self.checksum
    }

    #[must_use]
    pub fn signature(&self) -> &PluginSignature {
        &self.signature
    }
}

impl PluginPermission {
    pub fn parse(value: &str) -> Result<Self, DomainError> {
        match value {
            "tabs:read" => Ok(Self::TabsRead),
            "tabs:write" => Ok(Self::TabsWrite),
            "spaces:read" => Ok(Self::SpacesRead),
            "spaces:write" => Ok(Self::SpacesWrite),
            "bookmarks:read" => Ok(Self::BookmarksRead),
            "bookmarks:write" => Ok(Self::BookmarksWrite),
            "history:read" => Ok(Self::HistoryRead),
            "downloads:read" => Ok(Self::DownloadsRead),
            "downloads:write" => Ok(Self::DownloadsWrite),
            "page:metadata" => Ok(Self::PageMetadata),
            "page:screenshot" => Ok(Self::PageScreenshot),
            "page:script" => Ok(Self::PageScript),
            "clipboard:read" => Ok(Self::ClipboardRead),
            "clipboard:write" => Ok(Self::ClipboardWrite),
            "filesystem:read" => Ok(Self::FilesystemRead),
            "filesystem:write" => Ok(Self::FilesystemWrite),
            "network:fetch" => Ok(Self::NetworkFetch),
            "settings:read" => Ok(Self::SettingsRead),
            "settings:write" => Ok(Self::SettingsWrite),
            "sync:plugin" => Ok(Self::SyncPlugin),
            "ui:panel" => Ok(Self::UiPanel),
            "ui:command" => Ok(Self::UiCommand),
            "ui:context_menu" => Ok(Self::UiContextMenu),
            _ => Err(DomainError::InvalidPluginPermission { value: value.to_string() }),
        }
    }

    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::TabsRead => "tabs:read",
            Self::TabsWrite => "tabs:write",
            Self::SpacesRead => "spaces:read",
            Self::SpacesWrite => "spaces:write",
            Self::BookmarksRead => "bookmarks:read",
            Self::BookmarksWrite => "bookmarks:write",
            Self::HistoryRead => "history:read",
            Self::DownloadsRead => "downloads:read",
            Self::DownloadsWrite => "downloads:write",
            Self::PageMetadata => "page:metadata",
            Self::PageScreenshot => "page:screenshot",
            Self::PageScript => "page:script",
            Self::ClipboardRead => "clipboard:read",
            Self::ClipboardWrite => "clipboard:write",
            Self::FilesystemRead => "filesystem:read",
            Self::FilesystemWrite => "filesystem:write",
            Self::NetworkFetch => "network:fetch",
            Self::SettingsRead => "settings:read",
            Self::SettingsWrite => "settings:write",
            Self::SyncPlugin => "sync:plugin",
            Self::UiPanel => "ui:panel",
            Self::UiCommand => "ui:command",
            Self::UiContextMenu => "ui:context_menu",
        }
    }

    #[must_use]
    pub fn risk(&self) -> PluginPermissionRisk {
        match self {
            Self::TabsRead
            | Self::SpacesRead
            | Self::PageMetadata
            | Self::SettingsRead
            | Self::UiPanel
            | Self::UiCommand
            | Self::UiContextMenu => PluginPermissionRisk::Standard,
            Self::TabsWrite
            | Self::SpacesWrite
            | Self::BookmarksRead
            | Self::BookmarksWrite
            | Self::HistoryRead
            | Self::DownloadsRead
            | Self::DownloadsWrite
            | Self::PageScreenshot
            | Self::PageScript
            | Self::ClipboardRead
            | Self::ClipboardWrite
            | Self::FilesystemRead
            | Self::FilesystemWrite
            | Self::NetworkFetch
            | Self::SettingsWrite
            | Self::SyncPlugin => PluginPermissionRisk::High,
        }
    }

    #[must_use]
    pub fn requires_separate_confirmation(&self) -> bool {
        self.risk() == PluginPermissionRisk::High
    }
}

impl PluginContributionPoint {
    pub fn parse(value: &str) -> Result<Self, DomainError> {
        match value {
            "command-bar-command" => Ok(Self::CommandBarCommand),
            "tab-context-menu" => Ok(Self::TabContextMenu),
            "page-context-menu" => Ok(Self::PageContextMenu),
            "sidebar-panel" => Ok(Self::SidebarPanel),
            "settings-page" => Ok(Self::SettingsPage),
            "status-bar-indicator" => Ok(Self::StatusBarIndicator),
            "download-action" => Ok(Self::DownloadAction),
            "bookmark-action" => Ok(Self::BookmarkAction),
            "reading-mode-exporter" => Ok(Self::ReadingModeExporter),
            _ => Err(DomainError::InvalidPluginContribution { value: value.to_string() }),
        }
    }

    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::CommandBarCommand => "command-bar-command",
            Self::TabContextMenu => "tab-context-menu",
            Self::PageContextMenu => "page-context-menu",
            Self::SidebarPanel => "sidebar-panel",
            Self::SettingsPage => "settings-page",
            Self::StatusBarIndicator => "status-bar-indicator",
            Self::DownloadAction => "download-action",
            Self::BookmarkAction => "bookmark-action",
            Self::ReadingModeExporter => "reading-mode-exporter",
        }
    }
}

fn parse_unique_permissions(values: &[String]) -> Result<Vec<PluginPermission>, DomainError> {
    let mut seen = BTreeSet::new();
    let mut permissions = Vec::with_capacity(values.len());
    for value in values {
        let permission = PluginPermission::parse(value.as_str())?;
        if !seen.insert(permission.as_str()) {
            return Err(DomainError::DuplicatePluginPermission {
                value: permission.as_str().to_string(),
            });
        }
        permissions.push(permission);
    }
    Ok(permissions)
}

fn parse_unique_contributions(
    values: &[String],
) -> Result<Vec<PluginContributionPoint>, DomainError> {
    let mut seen = BTreeSet::new();
    let mut contributions = Vec::with_capacity(values.len());
    for value in values {
        let contribution = PluginContributionPoint::parse(value.as_str())?;
        if !seen.insert(contribution.as_str()) {
            return Err(DomainError::DuplicatePluginContribution {
                value: contribution.as_str().to_string(),
            });
        }
        contributions.push(contribution);
    }
    Ok(contributions)
}

fn non_empty_plugin_field(
    field: &'static str,
    value: impl Into<String>,
) -> Result<String, DomainError> {
    let value = value.into();
    let value = value.trim();
    if value.is_empty() {
        return Err(DomainError::EmptyField { field });
    }
    Ok(value.to_string())
}

fn plugin_homepage(value: impl Into<String>) -> Result<String, DomainError> {
    let value = non_empty_plugin_field("homepage", value)?;
    let parsed = Url::parse(value.as_str())
        .map_err(|_| DomainError::InvalidPluginHomepage { value: value.clone() })?;
    if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
        return Err(DomainError::InvalidPluginHomepage { value });
    }
    Ok(parsed.to_string())
}

fn parse_ely_build(value: impl Into<String>) -> Result<Version, DomainError> {
    let value = non_empty_plugin_field("min_ely_build", value)?;
    Version::parse(value.as_str()).map_err(|_| DomainError::InvalidPluginBuildRequirement { value })
}

fn plugin_checksum(value: impl Into<String>) -> Result<String, DomainError> {
    let value = non_empty_plugin_field("checksum", value)?;
    if !is_hex_of_len(value.as_str(), 64) {
        return Err(DomainError::InvalidPluginChecksum { value });
    }
    Ok(value.to_ascii_lowercase())
}

fn is_valid_plugin_id(value: &str) -> bool {
    (3..=128).contains(&value.len())
        && value.split('.').all(|segment| {
            !segment.is_empty()
                && segment.chars().all(|ch| {
                    ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '_'
                })
                && segment
                    .chars()
                    .next()
                    .is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
                && segment
                    .chars()
                    .last()
                    .is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
        })
}

fn is_hex_of_len(value: &str, len: usize) -> bool {
    value.len() == len && value.chars().all(|ch| ch.is_ascii_hexdigit())
}
