use crate::localization::{DeckPropertiesLabel as Detail, Localizer};
use crate::sync_identity::display_owner_identity;
use cosimo::{
    DeckVisibility, SyncDeckSummary, SyncDownloadKind, SyncRemoteDeckStatus, SyncSharedDeckStatus,
    SyncUuid,
};
use std::collections::BTreeSet;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RemoteDeckListEntry {
    Owner(SyncRemoteDeckStatus),
    Shared(SyncSharedDeckStatus),
}

impl RemoteDeckListEntry {
    pub(crate) fn deck_uuid(&self) -> &SyncUuid {
        match self {
            Self::Owner(status) => status.deck_uuid(),
            Self::Shared(status) => status.deck_uuid(),
        }
    }

    pub(crate) fn download_kind(&self) -> SyncDownloadKind {
        match self {
            Self::Owner(_) => SyncDownloadKind::OwnerStudyHistory,
            Self::Shared(_) => SyncDownloadKind::SharedContent,
        }
    }

    pub(crate) fn is_owned(&self) -> bool {
        matches!(self, Self::Owner(_))
    }

    pub(crate) fn label(&self, localizer: Localizer) -> String {
        match self {
            Self::Owner(status) => remote_deck_list_label(
                localizer,
                localizer.sync_remote_decks_owned_label(),
                status.summary(),
                status.deck_uuid(),
                status.visibility(),
                status.owner_identity(),
                true,
            ),
            Self::Shared(status) => remote_deck_list_label(
                localizer,
                deck_visibility_label(localizer, status.visibility()),
                status.summary(),
                status.deck_uuid(),
                status.visibility(),
                status.owner_identity(),
                false,
            ),
        }
    }

    pub(crate) fn details_text(&self, localizer: Localizer, server_url: &str) -> String {
        let mut report = String::new();
        match self {
            Self::Owner(status) => {
                push_remote_deck_status(localizer, &mut report, status, server_url)
            }
            Self::Shared(status) => {
                push_shared_deck_status(localizer, &mut report, status, server_url)
            }
        }
        report
    }
}

pub(crate) fn remote_deck_list_entries(
    owner_entries: Vec<SyncRemoteDeckStatus>,
    public_entries: Vec<SyncSharedDeckStatus>,
) -> Vec<RemoteDeckListEntry> {
    let owner_uuids: BTreeSet<SyncUuid> = owner_entries
        .iter()
        .map(|status| status.deck_uuid().clone())
        .collect();
    let mut entries: Vec<RemoteDeckListEntry> = owner_entries
        .into_iter()
        .map(RemoteDeckListEntry::Owner)
        .collect();
    entries.extend(
        public_entries
            .into_iter()
            .filter(|status| !owner_uuids.contains(status.deck_uuid()))
            .map(RemoteDeckListEntry::Shared),
    );
    entries
}

pub(crate) fn sync_deck_identifier_from_text(text: &str) -> Option<SyncUuid> {
    let text = trim_identifier_wrapper(text.trim());
    if text.is_empty() {
        return None;
    }
    if let Ok(uuid) = text.parse::<SyncUuid>() {
        return Some(uuid);
    }

    let before_fragment_or_query = text
        .find(['?', '#'])
        .map(|index| &text[..index])
        .unwrap_or(text);
    before_fragment_or_query
        .rsplit('/')
        .find_map(|segment| trim_identifier_wrapper(segment).parse::<SyncUuid>().ok())
}

fn trim_identifier_wrapper(text: &str) -> &str {
    text.trim_matches(|character| matches!(character, '<' | '>' | '"' | '\''))
}

pub(crate) fn sync_http_remote_decks_text(
    localizer: Localizer,
    server_url: &str,
    entries: &[SyncRemoteDeckStatus],
) -> String {
    let mut report = String::new();
    push_line(
        &mut report,
        localizer.options_sync_server_url_label(),
        server_url,
    );
    push_line(
        &mut report,
        localizer.sync_remote_decks_count_label(),
        &entries.len().to_string(),
    );
    push_line(
        &mut report,
        localizer.sync_remote_decks_invalid_count_label(),
        "0",
    );

    if entries.is_empty() {
        report.push('\n');
        report.push_str(localizer.sync_remote_decks_none_status());
        report.push('\n');
        return report;
    }

    for (index, status) in entries.iter().enumerate() {
        report.push('\n');
        report.push_str(&(index + 1).to_string());
        report.push_str(". ");
        push_remote_deck_status(localizer, &mut report, status, server_url);
    }

    report
}

fn push_remote_deck_status(
    localizer: Localizer,
    report: &mut String,
    status: &SyncRemoteDeckStatus,
    server_url: &str,
) {
    report.push_str(localizer.sync_status_remote_deck_label());
    report.push('\n');
    let has_summary = push_deck_summary_lines(localizer, report, status.summary());
    if !has_summary {
        push_line(
            report,
            localizer.deck_properties_label(Detail::DeckUuid),
            status.deck_uuid().as_str(),
        );
    }
    push_line(
        report,
        localizer.sync_status_remote_visibility_label(),
        deck_visibility_label(localizer, status.visibility()),
    );
    push_line(
        report,
        localizer.sync_status_remote_owner_identity_label(),
        &display_owner_identity(status.owner_identity()),
    );
    push_line(
        report,
        localizer.sync_status_remote_study_history_label(),
        if status.has_study_history() {
            localizer.sync_status_available()
        } else {
            localizer.card_details_none()
        },
    );
    push_line(
        report,
        localizer.sync_status_remote_last_upload_device_label(),
        &last_upload_device_text(localizer, status),
    );
    push_remote_deck_technical_lines(
        localizer,
        report,
        status.deck_uuid(),
        status.revision(),
        server_url,
        status.content_bundle_hash(),
        status.study_history_bundle_hash(),
    );
}

fn push_remote_deck_technical_lines(
    localizer: Localizer,
    report: &mut String,
    deck_uuid: &SyncUuid,
    revision: &str,
    server_url: &str,
    content_hash: &str,
    study_history_hash: Option<&str>,
) {
    report.push('\n');
    report.push_str(localizer.sync_details_technical_section_label());
    report.push('\n');
    push_line(
        report,
        localizer.options_sync_server_url_label(),
        server_url,
    );
    push_line(
        report,
        localizer.deck_properties_label(Detail::DeckUuid),
        deck_uuid.as_str(),
    );
    push_line(
        report,
        localizer.sync_status_remote_revision_label(),
        revision,
    );
    push_line(
        report,
        localizer.sync_remote_decks_content_hash_label(),
        content_hash,
    );
    if let Some(hash) = study_history_hash {
        push_line(
            report,
            localizer.sync_remote_decks_study_history_hash_label(),
            hash,
        );
    }
}

fn push_shared_deck_status(
    localizer: Localizer,
    report: &mut String,
    status: &SyncSharedDeckStatus,
    server_url: &str,
) {
    report.push_str(localizer.sync_status_remote_deck_label());
    report.push('\n');
    let has_summary = push_deck_summary_lines(localizer, report, status.summary());
    if !has_summary {
        push_line(
            report,
            localizer.deck_properties_label(Detail::DeckUuid),
            status.deck_uuid().as_str(),
        );
    }
    push_line(
        report,
        localizer.sync_status_remote_visibility_label(),
        deck_visibility_label(localizer, status.visibility()),
    );
    push_line(
        report,
        localizer.sync_status_remote_owner_identity_label(),
        &display_owner_identity(status.owner_identity()),
    );
    push_line(
        report,
        localizer.sync_status_remote_study_history_label(),
        localizer.sync_status_unavailable_owner_only(),
    );
    push_remote_deck_technical_lines(
        localizer,
        report,
        status.deck_uuid(),
        status.revision(),
        server_url,
        status.content_bundle_hash(),
        None,
    );
}

fn remote_deck_list_label(
    localizer: Localizer,
    scope: &str,
    summary: &SyncDeckSummary,
    deck_uuid: &SyncUuid,
    visibility: DeckVisibility,
    owner_identity: &str,
    include_visibility: bool,
) -> String {
    let mut parts = vec![
        scope.to_string(),
        remote_deck_display_name(summary, deck_uuid),
    ];
    if let Some(card_count) = summary.card_count() {
        parts.push(format!(
            "{}: {card_count}",
            localizer.sync_remote_decks_card_count_label()
        ));
    }
    if !summary.audio_formats().is_empty() {
        parts.push(format!(
            "{}: {}",
            localizer.sync_remote_decks_audio_formats_label(),
            summary.audio_formats().join(", ")
        ));
    }
    if include_visibility {
        parts.push(deck_visibility_label(localizer, visibility).to_string());
    }
    parts.push(display_owner_identity(owner_identity).into_owned());
    parts.join("; ")
}

fn last_upload_device_text(localizer: Localizer, status: &cosimo::SyncRemoteDeckStatus) -> String {
    match status.last_upload_device_name() {
        Some(name) => name.to_string(),
        None => localizer.card_details_none().to_string(),
    }
}

fn remote_deck_display_name(summary: &SyncDeckSummary, deck_uuid: &SyncUuid) -> String {
    summary
        .display_name()
        .map(str::to_string)
        .unwrap_or_else(|| deck_uuid.as_str().to_string())
}

fn push_deck_summary_lines(
    localizer: Localizer,
    report: &mut String,
    summary: &SyncDeckSummary,
) -> bool {
    let mut pushed = false;
    if let Some(title) = summary.title() {
        push_line(report, localizer.sync_remote_decks_title_label(), title);
        pushed = true;
    }
    if let Some(deck_name) = summary.deck_name() {
        push_line(
            report,
            localizer.sync_remote_decks_deck_name_label(),
            deck_name,
        );
        pushed = true;
    }
    if let Some(card_count) = summary.card_count() {
        push_line(
            report,
            localizer.sync_remote_decks_card_count_label(),
            &card_count.to_string(),
        );
        pushed = true;
    }
    if let Some(audio_file_count) = summary.audio_file_count() {
        push_line(
            report,
            localizer.sync_remote_decks_audio_files_label(),
            &audio_file_count.to_string(),
        );
        pushed = true;
    }
    if let Some(managed_audio_file_count) = summary.managed_audio_file_count() {
        push_line(
            report,
            localizer.sync_remote_decks_managed_audio_files_label(),
            &managed_audio_file_count.to_string(),
        );
        pushed = true;
    }
    if let Some(manual_audio_file_count) = summary.manual_audio_file_count() {
        push_line(
            report,
            localizer.sync_remote_decks_manual_audio_files_label(),
            &manual_audio_file_count.to_string(),
        );
        pushed = true;
    }
    if !summary.audio_formats().is_empty() {
        push_line(
            report,
            localizer.sync_remote_decks_audio_formats_label(),
            &summary.audio_formats().join(", "),
        );
        pushed = true;
    }
    pushed
}

fn deck_visibility_label(localizer: Localizer, visibility: DeckVisibility) -> &'static str {
    match visibility {
        DeckVisibility::Private => localizer.deck_visibility_private_choice(),
        DeckVisibility::Unlisted => localizer.deck_visibility_unlisted_choice(),
        DeckVisibility::Public => localizer.deck_visibility_public_choice(),
    }
}

fn push_line(report: &mut String, label: &str, value: &str) {
    report.push_str(label);
    report.push_str(": ");
    report.push_str(value);
    report.push('\n');
}

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

    fn shared_status(
        deck_uuid: SyncUuid,
        visibility: DeckVisibility,
        owner: &str,
    ) -> SyncSharedDeckStatus {
        SyncSharedDeckStatus::from_parts(deck_uuid, "1", visibility, owner, "sha256:content")
            .unwrap()
    }

    fn shared_status_with_summary(deck_uuid: SyncUuid) -> SyncSharedDeckStatus {
        SyncSharedDeckStatus::from_parts_with_summary(
            deck_uuid,
            "1",
            DeckVisibility::Public,
            "acct:modulus@example.invalid",
            "sha256:content",
            SyncDeckSummary::from_parts(
                Some("Indonesian"),
                Some("deck"),
                Some(2),
                Some(3),
                Some(2),
                vec!["opus".to_string(), "wav".to_string()],
            )
            .unwrap(),
        )
        .unwrap()
    }

    fn assert_before(text: &str, first: &str, second: &str) {
        let first_index = text
            .find(first)
            .unwrap_or_else(|| panic!("missing first text: {first}"));
        let second_index = text
            .find(second)
            .unwrap_or_else(|| panic!("missing second text: {second}"));
        assert!(
            first_index < second_index,
            "expected `{first}` before `{second}` in:\n{text}"
        );
    }

    #[test]
    fn remote_decks_report_lists_valid_remote_metadata() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let status = SyncRemoteDeckStatus::from_shared_status(
            shared_status_with_summary(deck_uuid),
            None,
            Some("desktop"),
        )
        .unwrap();

        let report =
            sync_http_remote_decks_text(Localizer::default(), "http://127.0.0.1:44791", &[status]);

        assert!(report.contains("Remote decks: 1"));
        assert!(report.contains("Malformed remote entries: 0"));
        assert!(report.contains("Deck UUID: 550e8400-e29b-41d4-a716-446655440000"));
        assert!(report.contains("Title: Indonesian"));
        assert!(report.contains("Deck name: deck"));
        assert!(report.contains("Cards: 2"));
        assert!(report.contains("Audio files: 3"));
        assert!(report.contains("Generated audio files: 2"));
        assert!(report.contains("Manual or untracked audio files: 1"));
        assert!(report.contains("Audio formats: opus, wav"));
        assert!(report.contains("Remote revision: 1"));
        assert!(report.contains("Remote visibility: Public"));
        assert!(report.contains("Remote study history: None"));
        assert!(report.contains("Remote last upload device: desktop"));
        assert_before(&report, "Title: Indonesian", "Deck UUID:");
        assert_before(&report, "Audio formats: opus, wav", "Technical details");
        assert_before(&report, "Technical details", "Content bundle hash:");
    }

    #[test]
    fn remote_decks_report_handles_empty_http_list() {
        let report =
            sync_http_remote_decks_text(Localizer::default(), "http://127.0.0.1:44791", &[]);

        assert!(report.contains("Remote decks: 0"));
        assert!(report.contains("Malformed remote entries: 0"));
        assert!(report.contains("No remote decks were found in the configured sync target."));
    }

    #[test]
    fn remote_deck_list_entries_keep_owner_entries_and_add_public_entries() {
        let owner_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let public_uuid: SyncUuid = "650e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let owner_entry = SyncRemoteDeckStatus::from_shared_status(
            shared_status(
                owner_uuid.clone(),
                DeckVisibility::Public,
                "acct:modulus@example.invalid",
            ),
            Some("sha256:history"),
            Some("desktop"),
        )
        .unwrap();

        let entries = remote_deck_list_entries(
            vec![owner_entry],
            vec![
                shared_status(
                    owner_uuid,
                    DeckVisibility::Public,
                    "acct:modulus@example.invalid",
                ),
                shared_status(
                    public_uuid.clone(),
                    DeckVisibility::Public,
                    "acct:other@example.invalid",
                ),
            ],
        );

        assert_eq!(entries.len(), 2);
        assert!(matches!(entries[0], RemoteDeckListEntry::Owner(_)));
        assert!(matches!(entries[1], RemoteDeckListEntry::Shared(_)));
        assert_eq!(entries[1].deck_uuid(), &public_uuid);
    }

    #[test]
    fn remote_deck_list_entries_select_correct_download_kind() {
        let owner_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let public_uuid: SyncUuid = "650e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let owner_entry = SyncRemoteDeckStatus::from_shared_status(
            shared_status(
                owner_uuid,
                DeckVisibility::Private,
                "acct:modulus@example.invalid",
            ),
            Some("sha256:history"),
            None,
        )
        .unwrap();
        let public_entry = shared_status(
            public_uuid,
            DeckVisibility::Public,
            "acct:other@example.invalid",
        );

        let owner_entry = RemoteDeckListEntry::Owner(owner_entry);
        let public_entry = RemoteDeckListEntry::Shared(public_entry);

        assert_eq!(
            owner_entry.download_kind(),
            SyncDownloadKind::OwnerStudyHistory
        );
        assert!(owner_entry.is_owned());
        assert_eq!(
            public_entry.download_kind(),
            SyncDownloadKind::SharedContent
        );
        assert!(!public_entry.is_owned());
    }

    #[test]
    fn remote_deck_list_labels_prioritize_readable_summary_over_technical_fields() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let entry = RemoteDeckListEntry::Shared(shared_status_with_summary(deck_uuid));

        let label = entry.label(Localizer::default());

        assert!(label.starts_with("Public; Indonesian; Cards: 2; Audio formats: opus, wav"));
        assert!(label.contains("@modulus@example.invalid"));
        assert_eq!(label.matches("Public").count(), 1);
        assert!(!label.contains("Remote revision"));
        assert!(!label.contains("550e8400-e29b-41d4-a716-446655440000"));
    }

    #[test]
    fn remote_deck_list_labels_show_shared_visibility() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let entry = RemoteDeckListEntry::Shared(shared_status(
            deck_uuid,
            DeckVisibility::Unlisted,
            "acct:other@example.invalid",
        ));

        let label = entry.label(Localizer::default());

        assert!(label.starts_with("Unlisted; 550e8400-e29b-41d4-a716-446655440000"));
        assert!(!label.starts_with("Public;"));
        assert_eq!(label.matches("Unlisted").count(), 1);
        assert!(label.contains("@other@example.invalid"));
    }

    #[test]
    fn shared_remote_deck_details_mark_owner_only_fields_unavailable() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let entry = RemoteDeckListEntry::Shared(shared_status(
            deck_uuid,
            DeckVisibility::Public,
            "acct:other@example.invalid",
        ));

        let details = entry.details_text(Localizer::default(), "https://sync.example.invalid");

        assert!(details.contains("Remote study history: Not available: owner-only information."));
        assert!(!details.contains("Remote study history: None"));
    }

    #[test]
    fn sync_deck_identifier_accepts_uuid_and_shared_urls() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();

        assert_eq!(
            sync_deck_identifier_from_text("550e8400-e29b-41d4-a716-446655440000"),
            Some(deck_uuid.clone())
        );
        assert_eq!(
            sync_deck_identifier_from_text(
                "https://sync.example.invalid/api/v1/decks/550e8400-e29b-41d4-a716-446655440000"
            ),
            Some(deck_uuid.clone())
        );
        assert_eq!(
            sync_deck_identifier_from_text(
                "<https://sync.example.invalid/api/v1/decks/550e8400-e29b-41d4-a716-446655440000/download?kind=shared>"
            ),
            Some(deck_uuid)
        );
        assert_eq!(sync_deck_identifier_from_text("not a deck id"), None);
    }
}
