// LoopFollow
// NightscoutSettingsViewModel.swift

import Combine
import Foundation
import SwiftUI

class NightscoutSettingsViewModel: ObservableObject {
    private var initialURL: String
    private var initialToken: String

    /// Whether the Nightscout connection is successfully verified
    @Published var isConnected: Bool = false

    /// Whether this is a fresh setup (URL was empty when view appeared)
    private(set) var isFreshSetup: Bool = false

    @Published var nightscoutURL: String = Storage.shared.url.value {
        willSet {
            if newValue != nightscoutURL {
                Storage.shared.url.value = newValue
                triggerCheckStatus()
            }
        }
    }

    @Published var nightscoutToken: String = Storage.shared.token.value {
        willSet {
            if newValue != nightscoutToken {
                Storage.shared.token.value = NightscoutUtils.sanitizeConnectionInput(newValue)
                triggerCheckStatus()
            }
        }
    }

    @Published var nightscoutStatus: String = "Checking..."

    /// The most recent verification error, kept so the onboarding address page can
    /// tell "reachable Nightscout that needs a token" apart from "can't reach it".
    @Published var lastError: NightscoutUtils.NightscoutError?

    /// True when the most recent error means the site is a reachable Nightscout
    /// that simply needs (a different) token.
    private var errorIsTokenRelated: Bool {
        switch lastError {
        case .tokenRequired, .invalidToken: return true
        default: return false
        }
    }

    /// The site responded as a Nightscout instance, even if it needs a token.
    var addressReachable: Bool {
        isConnected || errorIsTokenRelated
    }

    /// The site is reachable but requires a token we don't have yet.
    var addressNeedsToken: Bool {
        !isConnected && errorIsTokenRelated
    }

    @Published var webSocketEnabled: Bool = Storage.shared.webSocketEnabled.value {
        didSet {
            Storage.shared.webSocketEnabled.value = webSocketEnabled
            if webSocketEnabled {
                NightscoutSocketManager.shared.connectIfNeeded()
            } else {
                NightscoutSocketManager.shared.disconnect()
                triggerRefresh()
            }
        }
    }

    @Published var webSocketStatus: String = "Disconnected"

    var webSocketStatusColor: Color {
        switch NightscoutSocketManager.shared.connectionState {
        case .authenticated: return .green
        case .connecting, .connected: return .orange
        case .disconnected: return .secondary
        case .error: return .red
        }
    }

    private var cancellables = Set<AnyCancellable>()
    private var checkStatusSubject = PassthroughSubject<Void, Never>()
    private var checkStatusWorkItem: DispatchWorkItem?

    /// While confirming a freshly provisioned token, the retry loop owns the
    /// status label, so the ordinary debounced check is suppressed to avoid
    /// flickering "Invalid Token" before the server has caught up.
    private var isConfirmingProvisionedToken = false

    /// Set when a token we just created is correct (the create call returned its
    /// id, so the secret was valid and the token is a deterministic function of
    /// it) but the site hasn't started accepting it yet. Some hosts only reload
    /// their auth cache on a restart, which can take minutes — far longer than we
    /// can spin during onboarding — so this is treated as a success-pending state
    /// the user can proceed from, not an error.
    @Published private(set) var provisionedTokenPending = false

    /// True while a read-only token is being created from the API secret.
    @Published var isProvisioningToken = false

    /// The most recent token-provisioning failure, for inline display.
    @Published var tokenProvisionError: String?

    init() {
        initialURL = Storage.shared.url.value
        initialToken = Storage.shared.token.value
        isFreshSetup = initialURL.isEmpty

        setupDebounce()
        checkNightscoutStatus()
        observeWebSocketState()
    }

    private func setupDebounce() {
        checkStatusSubject
            .debounce(for: .seconds(2), scheduler: DispatchQueue.main)
            .sink { [weak self] in
                self?.checkNightscoutStatus()
            }
            .store(in: &cancellables)
    }

    private func triggerCheckStatus() {
        checkStatusWorkItem?.cancel()

        // Any manual edit invalidates a pending-provisioned state and any earlier
        // "this is your API secret" verdict.
        provisionedTokenPending = false
        tokenIsVerifiedSecret = false
        nightscoutStatus = "Checking..."

        checkStatusWorkItem = DispatchWorkItem {
            self.checkStatusSubject.send()
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: checkStatusWorkItem!)
    }

    func processURL(_ value: String) {
        // Strip whitespace/newlines/control chars first. A URL can't legally contain
        // them, and a stray one (e.g. a trailing newline from a paste) otherwise makes
        // URLComponents parsing fail and falls through to the lossy fallback below,
        // mangling a URL-with-embedded-token into an invalid address.
        let value = NightscoutUtils.sanitizeConnectionInput(value)

        var useTokenUrl = false

        if let urlComponents = URLComponents(string: value), let queryItems = urlComponents.queryItems {
            if let tokenItem = queryItems.first(where: { $0.name.lowercased() == "token" }) {
                let tokenPattern = "^[^-\\s]+-[0-9a-fA-F]{16}$"
                if let token = tokenItem.value, let _ = token.range(of: tokenPattern, options: .regularExpression) {
                    var baseComponents = urlComponents
                    baseComponents.queryItems = nil
                    if let baseURL = baseComponents.string {
                        nightscoutToken = token
                        nightscoutURL = baseURL
                        useTokenUrl = true
                    }
                }
            }
        }

        if !useTokenUrl {
            let filtered = value.replacingOccurrences(of: "[^A-Za-z0-9:/._-]", with: "", options: .regularExpression).lowercased()
            var cleanURL = filtered
            while cleanURL.count > 8, cleanURL.last == "/" {
                cleanURL = String(cleanURL.dropLast())
            }
            nightscoutURL = cleanURL
        }
    }

    func checkNightscoutStatus() {
        if isConfirmingProvisionedToken { return }
        NightscoutUtils.verifyURLAndToken { error, _, nsWriteAuth, nsAdminAuth in
            DispatchQueue.main.async {
                Storage.shared.nsWriteAuth.value = nsWriteAuth
                Storage.shared.nsAdminAuth.value = nsAdminAuth

                self.updateStatusLabel(error: error)
            }
        }
    }

    /// Applies a token that LoopFollow just created and confirms it works.
    ///
    /// A freshly created subject isn't always recognized immediately: each
    /// Nightscout server instance only reloads its in-memory subject cache on a
    /// write, and multi-instance deployments don't share that cache — so the
    /// first validation can be routed to an instance that hasn't caught up yet.
    /// Rather than fail (and make the user tap again), we poll for a few seconds
    /// with a reassuring status before surfacing any error.
    func confirmProvisionedToken(_ token: String) {
        isConfirmingProvisionedToken = true
        provisionedTokenPending = false
        isConnected = false
        nightscoutStatus = "Finishing connection…"
        nightscoutToken = token
        verifyProvisionedTokenLoop(attempt: 0)
    }

    private func verifyProvisionedTokenLoop(attempt: Int) {
        let maxAttempts = 8
        NightscoutUtils.verifyURLAndToken { [weak self] error, _, nsWriteAuth, nsAdminAuth in
            DispatchQueue.main.async {
                guard let self else { return }
                if error == nil {
                    self.isConfirmingProvisionedToken = false
                    self.provisionedTokenPending = false
                    Storage.shared.nsWriteAuth.value = nsWriteAuth
                    Storage.shared.nsAdminAuth.value = nsAdminAuth
                    self.updateStatusLabel(error: nil)
                } else if attempt + 1 < maxAttempts {
                    self.nightscoutStatus = "Finishing connection…"
                    let delay = min(0.5 + Double(attempt) * 0.25, 2.0)
                    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                        self.verifyProvisionedTokenLoop(attempt: attempt + 1)
                    }
                } else {
                    // The token is correct but the site hasn't started accepting
                    // it yet. Surface a calm "pending" state the user can proceed
                    // from rather than a red error.
                    self.isConfirmingProvisionedToken = false
                    self.provisionedTokenPending = true
                    self.lastError = nil
                }
            }
        }
    }

    // MARK: - Token provisioning from API secret

    /// Nightscout access tokens look like `name-<16 hex>`; anything else the user
    /// puts in the token field — most often their API secret — won't match.
    private static let tokenFormat = "^[^-\\s]+-[0-9a-fA-F]{16}$"

    private func looksLikeToken(_ value: String) -> Bool {
        value.range(of: Self.tokenFormat, options: .regularExpression) != nil
    }

    /// Set once the value in the token field has been confirmed to authenticate as
    /// the site's API secret. Drives the "create a token from this" suggestion.
    /// This is a verified result (an actual auth probe), not a format guess, so it
    /// only appears when creating a token will genuinely work.
    @Published private(set) var tokenIsVerifiedSecret = false

    /// Bumped on every edit so a slow verification for an old value can't land on a
    /// newer one.
    private var secretCheckGeneration = 0

    /// After a failed token check, find out whether what the user pasted is in fact
    /// the API secret — verified against the server, not guessed from its shape.
    private func verifyTokenIsSecret() {
        let candidate = nightscoutToken
        guard !candidate.isEmpty, !looksLikeToken(candidate) else {
            tokenIsVerifiedSecret = false
            return
        }
        secretCheckGeneration += 1
        let generation = secretCheckGeneration
        let url = nightscoutURL
        Task {
            let isSecret = await NightscoutUtils.verifyAPISecret(url: url, secret: candidate)
            await MainActor.run {
                guard generation == self.secretCheckGeneration,
                      self.nightscoutToken == candidate else { return }
                self.tokenIsVerifiedSecret = isSecret
            }
        }
    }

    /// Creates (or reuses) a read-only token from the given API secret and applies
    /// it. The secret authorizes the create call only and is never stored.
    func createReadOnlyToken(fromSecret secret: String) {
        let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        tokenProvisionError = nil
        isProvisioningToken = true
        let url = nightscoutURL

        Task {
            do {
                let token = try await NightscoutUtils.provisionReadOnlyToken(url: url, secret: trimmed)
                await MainActor.run {
                    self.isProvisioningToken = false
                    self.confirmProvisionedToken(token)
                }
            } catch {
                await MainActor.run {
                    self.isProvisioningToken = false
                    self.tokenProvisionError = NightscoutSettingsViewModel.provisioningMessage(for: error)
                }
            }
        }
    }

    static func provisioningMessage(for error: Error) -> String {
        guard let nsError = error as? NightscoutUtils.NightscoutError else {
            return "Could not create a token. Please try again."
        }
        switch nsError {
        case .invalidToken:
            return "That API secret was rejected. Check it and try again."
        case .invalidURL, .emptyAddress:
            return "Please enter a valid site URL first."
        case .siteNotFound:
            return "Couldn't reach that site. Check the URL."
        case .networkError:
            return "Network error. Check your connection and try again."
        case .tokenRequired, .unknown:
            return "Could not create a token. Please try again."
        }
    }

    func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
        lastError = error
        if let error = error {
            isConnected = false
            switch error {
            case .invalidURL:
                nightscoutStatus = "Invalid URL"
            case .networkError:
                nightscoutStatus = "Network Error"
            case .invalidToken:
                nightscoutStatus = "Invalid Token"
            case .tokenRequired:
                nightscoutStatus = "Token Required"
            case .siteNotFound:
                nightscoutStatus = "Site Not Found"
            case .unknown:
                nightscoutStatus = "Unknown Error"
            case .emptyAddress:
                nightscoutStatus = "Address Empty"
            }
            NightscoutSocketManager.shared.disconnect()

            // A site that's reachable but rejects the value as a token is the one
            // case where the user may have pasted their API secret — verify it so
            // we can offer to turn it into a token.
            switch error {
            case .invalidToken, .tokenRequired:
                verifyTokenIsSecret()
            default:
                tokenIsVerifiedSecret = false
            }
        } else {
            isConnected = true
            tokenIsVerifiedSecret = false
            let authStatus: String
            if Storage.shared.nsAdminAuth.value {
                authStatus = "Admin"
            } else {
                authStatus = "Read" + (Storage.shared.nsWriteAuth.value ? " & Write" : "")
            }

            nightscoutStatus = "OK (\(authStatus))"

            if nightscoutURL != initialURL || nightscoutToken != initialToken {
                NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
            }
        }
    }

    private func triggerRefresh() {
        NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
    }

    // MARK: - Adaptive status (onboarding)

    enum ConnectionStatusKind {
        case idle
        case checking
        case needsToken
        case pending
        case connected
        case error
    }

    /// A coarse status used to drive the onboarding status pill's color and icon.
    var statusKind: ConnectionStatusKind {
        if isConfirmingProvisionedToken { return .checking }
        if nightscoutURL.isEmpty { return .idle }
        if isConnected { return .connected }
        // Token created and correct, just not accepted by the site yet.
        if provisionedTokenPending { return .pending }
        if nightscoutStatus == "Checking..." { return .checking }
        // The site is reachable and simply needs a token — that's an expected
        // step, not an error, so it's shown positively rather than red.
        if addressNeedsToken { return .needsToken }
        return .error
    }

    /// A friendly, contextual status line that updates as the user fills fields,
    /// rather than a fixed "Status" label that can read as stale.
    var friendlyStatus: String {
        switch statusKind {
        case .idle:
            return "Enter your site address to connect."
        case .checking:
            return isConfirmingProvisionedToken ? "Finishing connection…" : "Checking your connection…"
        case .needsToken:
            return "Site found — it needs a token."
        case .pending:
            return "Token created. Your site can take a few minutes to start accepting it — you can continue."
        case .connected:
            if Storage.shared.nsAdminAuth.value { return "Connected — admin access" }
            if Storage.shared.nsWriteAuth.value { return "Connected — read & write" }
            return "Connected — read-only"
        case .error:
            return nightscoutStatus
        }
    }

    private func observeWebSocketState() {
        updateWebSocketStatus()
        NotificationCenter.default.publisher(for: .nightscoutSocketStateChanged)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.updateWebSocketStatus()
            }
            .store(in: &cancellables)
    }

    private func updateWebSocketStatus() {
        switch NightscoutSocketManager.shared.connectionState {
        case .disconnected: webSocketStatus = "Disconnected"
        case .connecting: webSocketStatus = "Connecting..."
        case .connected: webSocketStatus = "Connected"
        case .authenticated: webSocketStatus = "Connected"
        case .error: webSocketStatus = "Error"
        }
    }
}
