import Foundation

/// App-facing saved-device domain models, ported from `core/SavedDeviceModels.kt`.
/// The repository maps the generated UniFFI records into these so the UI never
/// depends on the binding surface directly.
///
/// A saved device is a remote VniDrop *app-installation identity*, not a person,
/// account, or piece of hardware. Display names and platform hints are untrusted
/// peer-supplied hints and must never be used to merge or match identities.

struct SavedDeviceModel: Equatable, Identifiable, Sendable {
	/// The remote iroh endpoint identity. Stable, cryptographic, and the only
	/// safe way to identify a peer.
	let endpointId: String
	/// User-owned local label. Takes precedence over `remoteDisplayName`.
	let localLabel: String?
	/// Untrusted display-name hint supplied by the peer.
	let remoteDisplayName: String?
	let createdAt: Int64
	let lastAuthenticatedAt: Int64?

	var id: String { endpointId }

	/// The name to show, or nil when neither side supplied one. Callers fall back
	/// to `L10n.SavedDevices.unnamed` (see `SavedDeviceTransferHistory.kt`).
	var displayNameOrNil: String? {
		if let localLabel, !localLabel.trimmed.isEmpty { return localLabel }
		if let remoteDisplayName, !remoteDisplayName.trimmed.isEmpty { return remoteDisplayName }
		return nil
	}
}

/// Durable consent lifecycle for one remote app-installation identity. A
/// relationship is usable only in `saved`; the pending states are bounded
/// operations that cannot initiate a transfer.
enum DeviceRelationshipStateModel: Equatable, Sendable {
	case pendingOutgoing
	case pendingIncoming
	case saved
	case revoked
	case blocked
}

struct DeviceRelationshipModel: Equatable, Identifiable, Sendable {
	let remoteEndpointId: String
	let state: DeviceRelationshipStateModel
	let generation: UInt64
	let minimumProtocolVersion: UInt16
	let createdAt: Int64
	let updatedAt: Int64

	var id: String { remoteEndpointId }
}

/// Single-use permission to *ask* to pair, created by a fully completed
/// authenticated transfer and expiring 24 hours later. Consumed by pairing,
/// declining, expiry, forget, block, or reset.
struct PairingEligibilityModel: Equatable, Identifiable, Sendable {
	let peerEndpointId: String
	/// Untrusted display-name hint from the qualifying transfer. Usually the only
	/// name available for a peer that is not saved yet.
	let remoteDisplayName: String?
	let sessionId: String
	let protocolVersion: UInt16
	let createdAt: Int64
	let expiresAt: Int64

	var id: String { peerEndpointId }
}

/// A pre-approval targeted offer awaiting a local approve/decline. Lives only in
/// the core's bounded live-session queue — a restart, timeout, disconnect, or
/// sender cancellation removes it, so it is never durable UI state.
struct PendingTargetedOfferModel: Equatable, Identifiable, Sendable {
	let transferId: String
	let senderEndpointId: String
	let receiverEndpointId: String
	let manifestId: String
	let contentHash: String
	/// Peer-supplied and untrusted; render it as text, never as a path.
	let transferName: String
	let fileCount: UInt64
	let totalSize: UInt64
	let protocolVersion: UInt16
	let receivedAt: Int64

	var id: String { transferId }
}

/// Durable targeted-transfer lifecycle. Rust validates every transition; the UI
/// only invokes typed operations and renders the snapshot it gets back.
enum TargetedTransferStateModel: Equatable, Sendable {
	case preparing
	case offering
	case awaitingApproval
	case approved
	case connecting
	case transferring
	case interrupted
	case completed
	case declined
	case cancelled
	case failed
	case deleted
}

struct TargetedTransferModel: Equatable, Identifiable, Sendable {
	let id: String
	let senderEndpointId: String
	let receiverEndpointId: String
	let manifestId: String
	/// Peer-supplied and untrusted; render it as text, never as a path.
	let transferName: String
	let fileCount: UInt64
	let totalSize: UInt64
	/// Bytes verified so far; survives interruption for resume.
	let verifiedBytes: UInt64
	let state: TargetedTransferStateModel
	let createdAt: Int64
	let updatedAt: Int64
}

/// Outcome of responding to a targeted offer. `alreadySettled` is the idempotent
/// replay path — the core returns the existing result rather than creating a
/// duplicate approval.
enum TargetedOfferResponseModel: Equatable, Sendable {
	case approved(transferId: String)
	case declined
	case alreadySettled(transferId: String)
}

// MARK: - Lifecycle helpers

extension TargetedTransferStateModel {
	/// States where bytes may still move, so progress is meaningful.
	var isActive: Bool {
		switch self {
		case .connecting, .transferring: return true
		default: return false
		}
	}

	/// No further transition is possible without creating a new transfer.
	var isTerminal: Bool {
		switch self {
		case .completed, .declined, .cancelled, .failed, .deleted: return true
		default: return false
		}
	}

	/// Cancellation withdraws the offer before approval and stops authorization
	/// plus active streaming after it. Terminal transfers have nothing to stop.
	var canCancel: Bool { !isTerminal }

	/// An interrupted transfer keeps its verified progress and resumes the same
	/// immutable transfer without asking for approval again.
	var canResume: Bool { self == .interrupted }

	/// The receiver pulls content once the sender's authorization is in place.
	var canReceive: Bool { self == .approved }

	/// Deletion makes authorization unusable and removes resumable state. Offered
	/// on anything already terminal except an entry that is itself deleted.
	var canDelete: Bool { isTerminal && self != .deleted }
}

extension TargetedTransferModel {
	/// Fraction of verified payload in `0...1`, or nil when the total is unknown
	/// or the state carries no meaningful progress.
	var progressFraction: Double? {
		guard totalSize > 0, state.isActive || state == .interrupted else { return nil }
		return min(1, Double(verifiedBytes) / Double(totalSize))
	}
}

extension PairingEligibilityModel {
	func isExpired(now: Int64) -> Bool { now >= expiresAt }
}

private extension String {
	var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }
}
