use anyhow::Context;
use iroh::RelayUrl;
use iroh_blobs::Hash;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, net::IpAddr, path::PathBuf, str::FromStr};

use crate::error::VnidropError;
use crate::util::{non_empty, now_ms};

pub(crate) const MAX_CUSTOM_RELAYS: usize = 8;
pub(crate) const MAX_RELAY_URL_BYTES: usize = 2_048;

/// Versions the saved-device domain seam and its wire protocols.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct SavedDeviceCapabilities {
    pub domain_contract_version: u16,
    pub relationship_protocol_version: u16,
    pub targeted_transfer_protocol_version: u16,
}

#[uniffi::export]
pub fn saved_device_capabilities() -> SavedDeviceCapabilities {
    SavedDeviceCapabilities {
        domain_contract_version: 1,
        relationship_protocol_version: 1,
        targeted_transfer_protocol_version: 3,
    }
}

/// Public view of a single-use pairing window after a completed transfer.
///
/// The eligibility capability itself never crosses this boundary.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct PairingEligibilitySummary {
    pub peer_endpoint_id: String,
    pub remote_display_name: Option<String>,
    pub session_id: String,
    pub protocol_version: u16,
    pub created_at: i64,
    pub expires_at: i64,
}

/// A remote VniDrop app-installation identity that completed mutual consent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct SavedDevice {
    pub endpoint_id: String,
    pub local_label: Option<String>,
    pub remote_display_name: Option<String>,
    pub created_at: i64,
    pub last_authenticated_at: Option<i64>,
}

/// Durable consent lifecycle for one remote app-installation identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum DeviceRelationshipState {
    PendingOutgoing,
    PendingIncoming,
    Saved,
    Revoked,
    Blocked,
}

/// Public relationship state; directional grant material remains core-private.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct DeviceRelationship {
    pub remote_endpoint_id: String,
    pub state: DeviceRelationshipState,
    pub generation: u64,
    pub minimum_protocol_version: u16,
    pub created_at: i64,
    pub updated_at: i64,
}

/// Rust-owned lifecycle for an immutable one-sender, one-receiver transfer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum TargetedTransferState {
    Preparing,
    Offering,
    AwaitingApproval,
    Approved,
    Connecting,
    Transferring,
    Interrupted,
    Completed,
    Declined,
    Cancelled,
    Failed,
    Deleted,
}

/// Immutable recipient-bound transfer snapshot, separate from an ordinary share.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct TargetedTransfer {
    pub id: String,
    pub sender_endpoint_id: String,
    pub receiver_endpoint_id: String,
    pub manifest_id: String,
    pub transfer_name: String,
    pub file_count: u64,
    pub total_size: u64,
    /// Bytes verified so far; survives interruption for resume.
    pub verified_bytes: u64,
    pub state: TargetedTransferState,
    pub created_at: i64,
    pub updated_at: i64,
}

/// Pre-approval offer summary. Deliberately omits any reusable share ticket.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct PendingTargetedOffer {
    pub transfer_id: String,
    pub sender_endpoint_id: String,
    pub receiver_endpoint_id: String,
    pub manifest_id: String,
    pub content_hash: String,
    pub transfer_name: String,
    pub file_count: u64,
    pub total_size: u64,
    pub protocol_version: u16,
    pub received_at: i64,
}

/// Local approve/decline outcome for a pending targeted offer.
///
/// Authorization stays in core custody; callers only receive transfer ids.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum TargetedOfferResponse {
    Approved { transfer_id: String },
    Declined,
    AlreadySettled { transfer_id: String },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum CoreRelayMode {
    Automatic,
    StrictCustom,
    CustomWithDirectFallback,
    LocalOnly,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct CoreNetworkConfig {
    pub mode: CoreRelayMode,
    pub relay_urls: Vec<String>,
}

impl Default for CoreNetworkConfig {
    fn default() -> Self {
        Self {
            mode: CoreRelayMode::Automatic,
            relay_urls: Vec::new(),
        }
    }
}

impl CoreNetworkConfig {
    pub(crate) fn validated_relay_urls(&self) -> anyhow::Result<Vec<RelayUrl>> {
        match self.mode {
            CoreRelayMode::Automatic | CoreRelayMode::LocalOnly => {
                if !self.relay_urls.is_empty() {
                    anyhow::bail!(
                        "{} relay mode must not include custom relay URLs",
                        match self.mode {
                            CoreRelayMode::Automatic => "automatic",
                            CoreRelayMode::LocalOnly => "local-only",
                            CoreRelayMode::StrictCustom
                            | CoreRelayMode::CustomWithDirectFallback => unreachable!(),
                        }
                    );
                }
                Ok(Vec::new())
            }
            CoreRelayMode::StrictCustom | CoreRelayMode::CustomWithDirectFallback => {
                if self.relay_urls.is_empty() {
                    anyhow::bail!("custom relay mode requires at least one relay URL");
                }
                if self.relay_urls.len() > MAX_CUSTOM_RELAYS {
                    anyhow::bail!(
                        "custom relay mode supports at most {MAX_CUSTOM_RELAYS} relay URLs"
                    );
                }

                let mut seen = BTreeSet::new();
                let mut validated = Vec::with_capacity(self.relay_urls.len());
                for (index, value) in self.relay_urls.iter().enumerate() {
                    if value.is_empty()
                        || value
                            .chars()
                            .any(|character| character.is_whitespace() || character.is_control())
                    {
                        anyhow::bail!(
                            "relay URL must be non-empty and contain no whitespace or control characters"
                        );
                    }
                    if value.len() > MAX_RELAY_URL_BYTES {
                        anyhow::bail!(
                            "relay URL is {} bytes, limit is {MAX_RELAY_URL_BYTES}",
                            value.len()
                        );
                    }
                    let url = RelayUrl::from_str(value)
                        .with_context(|| format!("invalid relay URL at position {}", index + 1))?;
                    let secure = url.scheme() == "https";
                    let loopback_http = url.scheme() == "http"
                        && url.host_str().is_some_and(|host| {
                            host.eq_ignore_ascii_case("localhost")
                                || host
                                    .trim_start_matches('[')
                                    .trim_end_matches(']')
                                    .parse::<IpAddr>()
                                    .is_ok_and(|address| address.is_loopback())
                        });
                    if !secure && !loopback_http {
                        anyhow::bail!(
                            "relay URL must use HTTPS; HTTP is allowed only for loopback development relays"
                        );
                    }
                    if url.host_str().is_none() {
                        anyhow::bail!("relay URL must include a host");
                    }
                    if url.port() == Some(0) {
                        anyhow::bail!("relay URL port must be between 1 and 65535");
                    }
                    if value.contains('@') || !url.username().is_empty() || url.password().is_some()
                    {
                        anyhow::bail!("relay URL must not contain credentials");
                    }
                    if url.query().is_some() || url.fragment().is_some() {
                        anyhow::bail!("relay URL must not contain a query or fragment");
                    }
                    if url.path() != "/" {
                        anyhow::bail!("relay URL must not contain a path");
                    }
                    if !seen.insert(url.clone()) {
                        anyhow::bail!("custom relay URLs must not contain duplicates");
                    }
                    validated.push(url);
                }
                Ok(validated)
            }
        }
    }
}

#[uniffi::export]
pub fn default_core_network_config() -> CoreNetworkConfig {
    CoreNetworkConfig::default()
}

/// Removes the blob store after its owning core has shut down.
///
/// Callers must verify that no transfer or share is active before shutdown.
#[uniffi::export]
pub fn clear_inactive_transfer_cache(app_data_dir: String) -> Result<u64, VnidropError> {
    let result = (|| -> anyhow::Result<u64> {
        let app_data_dir = PathBuf::from(app_data_dir);
        anyhow::ensure!(
            app_data_dir.is_absolute(),
            "app data directory must be absolute"
        );
        anyhow::ensure!(
            app_data_dir.file_name().is_some(),
            "app data directory must not be a filesystem root"
        );
        let blobs = app_data_dir.join("blobs");
        let metadata = match std::fs::symlink_metadata(&blobs) {
            Ok(metadata) => metadata,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
            Err(error) => return Err(error.into()),
        };
        anyhow::ensure!(
            metadata.is_dir() && !metadata.file_type().is_symlink(),
            "blob store path is not a directory"
        );
        let bytes = walkdir::WalkDir::new(&blobs)
            .follow_links(false)
            .into_iter()
            .try_fold(0u64, |total, entry| {
                let entry = entry?;
                let metadata = entry.metadata()?;
                Ok::<_, walkdir::Error>(if metadata.is_file() {
                    total.saturating_add(metadata.len())
                } else {
                    total
                })
            })?;
        std::fs::remove_dir_all(blobs)?;
        Ok(bytes)
    })();
    result.map_err(VnidropError::filesystem)
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct CoreLimits {
    pub max_sources: u64,
    pub max_collection_files: u64,
    pub max_total_bytes: u64,
    pub max_path_bytes: u64,
    pub max_ticket_bytes: u64,
    pub max_metadata_bytes: u64,
    pub max_events: u64,
    pub max_pending_approvals: u64,
    /// Incoming pairing / targeted offers awaiting the local user's decision.
    pub max_pending_offers: u64,
    pub max_concurrent_transfers: u64,
    pub event_queue_capacity: u64,
    /// Cap on Saved + pending mutual-consent relationships.
    pub max_saved_devices: u64,
    /// Quiet period after a decline or repeated malformed control-plane traffic.
    pub identity_cooldown_ms: u64,
    /// Malformed control-plane messages from one identity before cooldown.
    pub malformed_strike_limit: u64,
    /// Pairing RPC / acknowledgement wait bound (milliseconds).
    pub pairing_timeout_ms: u64,
    /// Pre-approval offer decision wait bound (milliseconds).
    pub offer_timeout_ms: u64,
    /// Connection establishment bound for targeted transfers (milliseconds).
    pub connection_timeout_ms: u64,
}

impl Default for CoreLimits {
    fn default() -> Self {
        Self {
            max_sources: 128,
            max_collection_files: 10_000,
            // Cap extreme disk fill while still allowing multi-GB folders.
            max_total_bytes: 256 * 1024 * 1024 * 1024,
            max_path_bytes: 4_096,
            // vnd1 tickets are small JSON+base64; 256 KiB is a generous ceiling.
            max_ticket_bytes: 256 * 1024,
            max_metadata_bytes: 16 * 1024,
            max_events: 500,
            // Bound handshake spam / notification pressure on the sender.
            max_pending_approvals: 64,
            // A pairing prompt needs the user in front of the device, so this
            // is far smaller than the handshake queue.
            max_pending_offers: 16,
            max_concurrent_transfers: 8,
            event_queue_capacity: 1_024,
            max_saved_devices: 256,
            identity_cooldown_ms: 60_000,
            malformed_strike_limit: 5,
            pairing_timeout_ms: 15_000,
            offer_timeout_ms: 120_000,
            connection_timeout_ms: 30_000,
        }
    }
}

impl CoreLimits {
    pub(crate) fn validate(&self) -> anyhow::Result<()> {
        let positive = [
            ("max_sources", self.max_sources),
            ("max_collection_files", self.max_collection_files),
            ("max_total_bytes", self.max_total_bytes),
            ("max_path_bytes", self.max_path_bytes),
            ("max_ticket_bytes", self.max_ticket_bytes),
            ("max_metadata_bytes", self.max_metadata_bytes),
            ("max_events", self.max_events),
            ("max_pending_approvals", self.max_pending_approvals),
            ("max_pending_offers", self.max_pending_offers),
            ("max_concurrent_transfers", self.max_concurrent_transfers),
            ("event_queue_capacity", self.event_queue_capacity),
            ("max_saved_devices", self.max_saved_devices),
            ("identity_cooldown_ms", self.identity_cooldown_ms),
            ("malformed_strike_limit", self.malformed_strike_limit),
            ("pairing_timeout_ms", self.pairing_timeout_ms),
            ("offer_timeout_ms", self.offer_timeout_ms),
            ("connection_timeout_ms", self.connection_timeout_ms),
        ];
        for (name, value) in positive {
            if value == 0 {
                anyhow::bail!("core limit {name} must be greater than zero");
            }
        }
        for (name, value) in [
            ("max_pending_approvals", self.max_pending_approvals),
            ("max_pending_offers", self.max_pending_offers),
            ("max_concurrent_transfers", self.max_concurrent_transfers),
            ("event_queue_capacity", self.event_queue_capacity),
            ("max_saved_devices", self.max_saved_devices),
            ("malformed_strike_limit", self.malformed_strike_limit),
        ] {
            usize::try_from(value)
                .with_context(|| format!("core limit {name} exceeds platform capacity"))?;
        }
        if self.max_total_bytes > i64::MAX as u64 || self.max_events > i64::MAX as u64 {
            anyhow::bail!("SQLite-backed limits must fit in a signed 64-bit integer");
        }
        Ok(())
    }

    pub(crate) fn validate_metadata_text(
        &self,
        field: &str,
        value: Option<&str>,
    ) -> anyhow::Result<()> {
        if let Some(value) = value {
            if value.len() as u64 > self.max_metadata_bytes {
                anyhow::bail!(
                    "{field} is {} bytes, limit is {}",
                    value.len(),
                    self.max_metadata_bytes
                );
            }
        }
        Ok(())
    }
}

#[uniffi::export]
pub fn default_core_limits() -> CoreLimits {
    CoreLimits::default()
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct CoreEvent {
    pub id: String,
    /// Monotonic per-process revision for at-least-once delivery deduplication.
    pub revision: u64,
    pub timestamp: i64,
    pub scope: String,
    pub transfer_id: Option<u64>,
    pub direction: Option<String>,
    pub phase: String,
    pub kind: String,
    pub data_json: String,
}

#[uniffi::export(with_foreign)]
pub trait CoreEventSink: Send + Sync {
    fn on_event(&self, event: CoreEvent);
}

#[uniffi::export(with_foreign)]
pub trait ReceiveOutputSink: Send + Sync {
    fn start_file(&self, relative_path: String) -> Result<(), crate::error::VnidropError>;
    fn write_chunk(
        &self,
        relative_path: String,
        bytes: Vec<u8>,
    ) -> Result<(), crate::error::VnidropError>;
    fn finish_file(&self, relative_path: String) -> Result<(), crate::error::VnidropError>;
    fn abort_file(
        &self,
        relative_path: String,
        reason: String,
    ) -> Result<(), crate::error::VnidropError>;
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum ReceivedLocatorKind {
    FilesystemPath,
    AndroidMediaStore,
    AndroidDocument,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct PublishedOutput {
    pub locator_kind: ReceivedLocatorKind,
    pub locator: String,
}

/// Versioned receive sink that reports the durable locator created at publish time.
#[uniffi::export(with_foreign)]
pub trait ReceiveOutputSinkV2: Send + Sync {
    fn start_file(&self, relative_path: String) -> Result<(), crate::error::VnidropError>;
    fn write_chunk(
        &self,
        relative_path: String,
        bytes: Vec<u8>,
    ) -> Result<(), crate::error::VnidropError>;
    fn finish_file(
        &self,
        relative_path: String,
    ) -> Result<PublishedOutput, crate::error::VnidropError>;
    fn abort_file(
        &self,
        relative_path: String,
        reason: String,
    ) -> Result<(), crate::error::VnidropError>;
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct ReceivedArtifact {
    pub id: String,
    pub transfer_local_id: String,
    pub protocol_transfer_id: u64,
    pub relative_path: String,
    pub locator_kind: ReceivedLocatorKind,
    pub locator: String,
    pub logical_size: u64,
    pub published_at: i64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct CoreStorageUsage {
    pub blob_store_bytes: u64,
    pub database_bytes: u64,
    pub logs_bytes: u64,
    pub previews_bytes: u64,
    pub other_core_bytes: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct RuntimeStatus {
    pub endpoint_id: String,
    pub addr: String,
    pub active_transfers: u64,
    pub active_shares: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
pub enum SourceKind {
    Path,
    FileDescriptor,
    AndroidContentUri,
    IosSecurityScopedUrl,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ShareSource {
    pub kind: SourceKind,
    pub value: String,
    pub display_name: Option<String>,
    pub is_directory: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ShareMetadataInput {
    pub transfer_id: u64,
    pub transfer_name: Option<String>,
    pub sender_name: Option<String>,
    pub access_mode: TransferAccessMode,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum TransferAccessMode {
    Public,
    ApprovalRequired,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct StoredTransfer {
    pub local_id: String,
    pub transfer_id: u64,
    pub peer_id: Option<String>,
    pub direction: String,
    pub status: String,
    pub transfer_name: Option<String>,
    pub content_hash: Option<String>,
    pub ticket: Option<String>,
    pub file_count: u64,
    pub total_size: u64,
    pub access_mode: TransferAccessMode,
    pub created_at: i64,
    pub updated_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ShareResult {
    pub transfer_id: u64,
    pub ticket: String,
    pub hash: String,
    pub transfer_name: String,
    pub file_count: u64,
    pub total_size: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct TransferMetadata {
    pub version: u8,
    pub transfer_id: u64,
    pub transfer_name: String,
    pub sender_name: Option<String>,
    pub created_at: i64,
    pub content_hash: String,
    pub file_count: u64,
    pub total_size: u64,
}

impl TransferMetadata {
    pub(crate) fn new(
        transfer_id: u64,
        transfer_name: impl Into<String>,
        sender_name: Option<String>,
        content_hash: Hash,
        file_count: u64,
        total_size: u64,
    ) -> Self {
        Self {
            version: 1,
            transfer_id,
            transfer_name: transfer_name.into(),
            sender_name: sender_name.and_then(non_empty),
            created_at: now_ms(),
            content_hash: content_hash.to_string(),
            file_count,
            total_size,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct TicketInspection {
    pub kind: String,
    pub metadata: TransferMetadata,
}

#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ReceiverRequest {
    pub id: String,
    pub transfer_id: u64,
    pub remote_endpoint_id: String,
    pub transfer_name: String,
    pub receiver_name: Option<String>,
    pub receiver_device_name: Option<String>,
    pub app_version: String,
    pub status: String,
    pub reason: Option<String>,
    pub requested_at: i64,
    pub responded_at: Option<i64>,
    pub completed_at: Option<i64>,
}
