// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Thibault Ducray
//
// This file is part of MyPwdTool's open-source sync/encryption core — see
// LICENSE-SYNC-CRYPTO.md at the repo root and https://tducray.fr/mypwdtool/open-source/.
// The rest of this application is proprietary and NOT covered by this license.

import Foundation

// MARK: - SyncStatus

enum SyncStatus {
    case disabled       // sync not configured
    case idle           // configured, not syncing
    case syncing        // actively syncing
    case error(String)  // last sync error

    var isActive: Bool {
        if case .syncing = self { return true }
        return false
    }
}

// MARK: - ServerLimits

/// Limits as returned by GET /limits
struct ServerLimits: Codable {
    var maxMessageBytes: Int
    var maxMessagesPerDay: Int
    var defaultTtlSeconds: Int
    var maxTtlSeconds: Int
    var maxRecipientsPerMessage: Int

    enum CodingKeys: String, CodingKey {
        case maxMessageBytes        = "max_message_bytes"
        case maxMessagesPerDay      = "max_messages_per_day"
        case defaultTtlSeconds      = "default_ttl_seconds"
        case maxTtlSeconds          = "max_ttl_seconds"
        case maxRecipientsPerMessage = "max_recipients_per_message"
    }

    static let `default` = ServerLimits(
        maxMessageBytes: 262144,
        maxMessagesPerDay: 20000,
        defaultTtlSeconds: 1296000,
        maxTtlSeconds: 2592000,
        maxRecipientsPerMessage: 50
    )
}

// MARK: - SyncPeer

/// A peer device in our sync group
struct SyncPeer: Codable, Identifiable {
    var id: String { inboxId }
    var inboxId: String
    var deviceName: String
    var addedAt: Date
    var relayDeviceId: String?   // relay-level device ID; set on hello, used for lastSeenAt
    var lastSeenAt: Date?
    var isSuspended: Bool = false

    enum CodingKeys: String, CodingKey {
        case inboxId        = "inbox_id"
        case deviceName     = "device_name"
        case addedAt        = "added_at"
        case relayDeviceId  = "relay_device_id"
        case lastSeenAt     = "last_seen_at"
        case isSuspended    = "is_suspended"
    }
}

// MARK: - SyncOp

enum SyncOp: String, Codable {
    case upsert
    case delete
    case deviceHello  = "device_hello"
    case deviceLeave  = "device_leave"
    case deviceRevoke = "device_revoke"
    case keyRotate    = "key_rotate"
    case heartbeat    = "heartbeat"
}

// MARK: - SyncPasswordHistoryItem

/// One archived past password for an entry, carried inside a `SyncPayload` upsert so a device
/// that joins the group (or was offline) mid-history gets the full trail, not just whatever it
/// happened to be connected for. Plaintext in transit like `password`/`previousPassword` — the
/// whole payload is already sealed under the group's SGK.
struct SyncPasswordHistoryItem: Codable {
    var id: String
    var changedAt: String   // ISO8601
    var password: String

    enum CodingKeys: String, CodingKey {
        case id
        case changedAt = "changed_at"
        case password
    }
}

// MARK: - SyncPayload

/// The payload placed inside the relay ciphertext (plaintext before SGK encryption)
struct SyncPayload: Codable {
    var schema: Int = 1
    var eventId: String           // UUIDv4, for idempotency
    var senderDeviceId: String
    var senderDeviceName: String?  // populated when sending; nil on old messages
    var senderCounter: Int
    var op: SyncOp
    // for upsert + delete:
    var entryId: String?
    var updatedAt: String?        // ISO8601
    // for upsert only (plaintext sensitive fields):
    var name: String?
    var username: String?
    var website: String?           // legacy — kept for backward compat with older clients
    var websites: [String]?        // multiple URLs; supersedes website when present
    var password: String?
    var previousPassword: String?
    // Full password history for this entry, so a device joining the group (or catching up after
    // being offline) gets the complete trail from a single upsert rather than only whatever
    // individual change messages it happened to receive. See PasswordHistoryEntry.
    var passwordHistory: [SyncPasswordHistoryItem]?
    var note: String?
    var cardNumber: String?
    var cardExpiry: String?
    var cardCVV: String?
    var cardPin: String?
    var totpSecret: String?
    var entryType: String?
    var isFavorite: Bool?
    // Bin state — non-nil means the entry is in the Bin as of this timestamp (ISO8601), nil
    // means active/restored. Syncs like any other field via upsert, so all devices agree on
    // Bin membership; permanent removal still goes through the existing .delete op (either the
    // user emptying the Bin, or the 30-day auto-purge — see BinPurger).
    var deletedAt: String?
    var passkeyRelyingPartyId: String?
    var passkeyCredentialId: String?   // plaintext in transit (like password)
    var passkeyUserHandle: String?     // plaintext in transit
    var createdAt: String?
    // Sender's own inbox ID — set on device_hello, device_leave, heartbeat, upsert, and delete
    // (added 2026-07-27 to the latter three: msg.senderDeviceId, the relay-assigned identifier
    // previously used to match a message to a known peer for "last seen" tracking, was observed
    // live to silently drift out of sync with what a peer actually sends, permanently blocking
    // that peer's last-seen from ever advancing again — inboxId is authoritative and never
    // drifts, since it's the same identifier that already, correctly, routes every message).
    // EXCEPTION: on device_revoke this instead carries the TARGET's inbox (the device being
    // revoked), not the sender's own — see SyncManager.revokeDevice.
    var inboxId: String?
    var deviceName: String?
    // for key_rotate: the new SGK, base64url-encoded. This whole SyncPayload (including
    // this field) is protected only by the envelope-level AES-GCM, which for a key_rotate
    // message is deliberately sealed under the OLD SGK — see SyncManager.sendKeyRotation.
    var newSgk: String?

    enum CodingKeys: String, CodingKey {
        case schema, op
        case eventId          = "event_id"
        case senderDeviceId   = "sender_device_id"
        case senderDeviceName = "sender_device_name"
        case senderCounter    = "sender_counter"
        case entryId        = "entry_id"
        case updatedAt      = "updated_at"
        case name, username, website, websites, password, note
        case previousPassword = "previous_password"
        case passwordHistory  = "password_history"
        case cardNumber     = "card_number"
        case cardExpiry     = "card_expiry"
        case cardCVV        = "card_cvv"
        case cardPin        = "card_pin"
        case totpSecret     = "totp_secret"
        case entryType      = "entry_type"
        case isFavorite     = "is_favorite"
        case deletedAt      = "deleted_at"
        case passkeyRelyingPartyId = "passkey_rp_id"
        case passkeyCredentialId   = "passkey_credential_id"
        case passkeyUserHandle     = "passkey_user_handle"
        case createdAt      = "created_at"
        case inboxId        = "inbox_id"
        case deviceName     = "device_name"
        case newSgk         = "new_sgk"
    }
}

// MARK: - PairingCode

/// The pairing code shared between devices (base64url-encoded JSON)
struct PairingCode: Codable {
    var version: Int = 1
    var serverURL: String
    var streamId: String
    var sgk: String               // base64url 32 bytes
    var inboxId: String           // sender's inbox (so recipient can reply)
    var deviceName: String

    enum CodingKeys: String, CodingKey {
        case version    = "v"
        case serverURL  = "url"
        case streamId   = "stream_id"
        case sgk
        case inboxId    = "inbox_id"
        case deviceName = "device_name"
    }
}

// MARK: - SyncProgress

/// Progress during initial bulk sync
struct SyncProgress {
    var sent: Int
    var total: Int
    var isReceiving: Bool
}
