import Foundation
import NetworkExtension

@MainActor
final class MainViewModel: ObservableObject {
    static let companionURL = URL(string: "https://portal.hivetechnology.net/mobile-vpn?platform=ios")!

    struct State {
        var phase = "idle"
        var title = ""
        var detail = ""
        var deviceName = ""
        var isLoading = false
        var error: String?
    }

    @Published private(set) var state = State()
    @Published private(set) var diagnostics: TunnelDiagnostics?
    @Published var isPortalPresented = false
    @Published private(set) var portalReturnURL: URL? = MainViewModel.companionURL
    let tunnel = PacketTunnelManager()
    private let api = MobileVpnAPI()
    private let store = SecureSessionStore()
    private var credentials: SessionCredentials?
    private var pendingTicket: String?
    private var disconnectRecoveryTask: Task<Void, Never>?
    private var disconnectTask: Task<Void, Never>?
    private var diagnosticsTask: Task<Void, Never>?
    private var hasObservedRuntimeConnection = false

    init() {
        state = idleState()
#if DEBUG
        if let preview = ProcessInfo.processInfo.environment["HT_STORE_PREVIEW"] {
            showStorePreview(preview)
            return
        }
#endif
        credentials = try? store.load()
        if let storedReturnUrl = validatedPortalReturnURL(credentials?.portalReturnUrl) {
            portalReturnURL = storedReturnUrl
        }
        Task {
            try? await tunnel.load()
            refreshFromTunnelStatus()
        }
        diagnosticsTask = Task { [weak self] in
            while !Task.isCancelled {
                await self?.refreshDiagnostics()
                try? await Task.sleep(for: .seconds(1))
            }
        }
    }

    func handle(url: URL) {
        isPortalPresented = false
        disconnectRecoveryTask?.cancel()
        disconnectRecoveryTask = nil
        if let returnURL = portalReturnURL(from: url) {
            portalReturnURL = returnURL
        }
        guard let ticket = handoffTicket(from: url) else {
            state.error = L10n.text(
                "state.invalid_link",
                fallback: "The VPN link is invalid or incomplete."
            )
            return
        }
        guard credentials == nil else {
            state.error = L10n.text(
                "state.active_conflict",
                fallback: "Disconnect the active VPN before opening another one."
            )
            return
        }
        pendingTicket = ticket
        state = State(
            phase: "privacy-consent",
            title: L10n.text("privacy.title", fallback: "Before you connect"),
            detail: L10n.text(
                "privacy.summary",
                fallback: "Review how HT Portal VPN handles data for the requested connection."
            )
        )
    }

    func confirmVpnDisclosure() {
        guard credentials == nil, let ticket = pendingTicket else { return }
        pendingTicket = nil
        state = State(
            phase: "redeeming",
            title: L10n.text("state.redeeming.title", fallback: "Verifying request"),
            detail: L10n.text("state.redeeming.detail", fallback: "Secure connection to HT Portal"),
            isLoading: true
        )
        Task { await redeem(ticket) }
    }

    func cancelVpnDisclosure() {
        pendingTicket = nil
        state = State(
            phase: "cancelled",
            title: L10n.text("state.cancelled.title", fallback: "VPN not started"),
            detail: L10n.text(
                "state.cancelled.detail",
                fallback: "No VPN session data was sent by this app. You can return to HT Portal."
            )
        )
    }

    func showPortal() {
        isPortalPresented = true
    }

    private func portalReturnURL(from url: URL) -> URL? {
        let rawValue = URLComponents(url: url, resolvingAgainstBaseURL: false)?
            .queryItems?.first(where: { $0.name == "return" })?.value
        return validatedPortalReturnURL(rawValue)
    }

    private func validatedPortalReturnURL(_ rawValue: String?) -> URL? {
        guard let rawValue,
              let candidate = URL(string: rawValue),
              let components = URLComponents(url: candidate, resolvingAgainstBaseURL: false),
              components.scheme == "https",
              components.host == "portal.hivetechnology.net",
              components.user == nil,
              components.password == nil,
              (components.port == nil || components.port == 443),
              components.path == "/mobile-vpn" || components.path.hasPrefix("/mobile-vpn/") else {
            return nil
        }
        return candidate
    }

    private func handoffTicket(from url: URL) -> String? {
        guard let fragment = URLComponents(url: url, resolvingAgainstBaseURL: false)?.fragment else {
            return nil
        }
        for item in fragment.split(separator: "&") {
            let pair = item.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
            guard pair.count == 2, pair[0] == "handoff" else { continue }
            let ticket = String(pair[1])
            guard (40...128).contains(ticket.utf8.count) else { return nil }
            let isBase64URL = ticket.utf8.allSatisfy { byte in
                (byte >= 48 && byte <= 57) ||
                    (byte >= 65 && byte <= 90) ||
                    (byte >= 97 && byte <= 122) ||
                    byte == 45 || byte == 95
            }
            return isBase64URL ? ticket : nil
        }
        return nil
    }

    func disconnect() {
        guard state.phase != "stopping" else { return }
        disconnectRecoveryTask?.cancel()
        disconnectRecoveryTask = nil
        let current = credentials
        let deviceName = current?.deviceName ?? state.deviceName
        state = State(
            phase: "stopping",
            title: L10n.text("state.stopping.title", fallback: "Disconnecting"),
            detail: L10n.text(
                "state.stopping.detail",
                fallback: "Stopping the VPN and updating the portal."
            ),
            deviceName: deviceName,
            isLoading: true
        )
        tunnel.stop()
        disconnectTask?.cancel()
        disconnectTask = Task { [weak self] in
            await self?.completeDisconnect(current, deviceName: deviceName)
        }
    }

    func refreshFromTunnelStatus() {
        guard tunnel.isLoaded else { return }
        if tunnel.isUserRequestedStop {
            if state.phase != "stopping" && state.phase != "stop-failed" && state.phase != "stopped" {
                state = stoppedState()
            }
            return
        }
        let deviceName = credentials?.deviceName ?? state.deviceName
        switch tunnel.status {
        case .connected:
            disconnectRecoveryTask?.cancel()
            disconnectRecoveryTask = nil
            if diagnostics?.runtimeConnected == true {
                state = connectedState(deviceName: deviceName)
            } else {
                state = checkingState(deviceName: deviceName)
            }
        case .connecting:
            state = connectingState(deviceName: deviceName)
        case .reasserting:
            state = hasObservedRuntimeConnection
                ? connectedState(deviceName: deviceName)
                : connectingState(deviceName: deviceName)
        case .disconnecting:
            state = State(
                phase: "stopping",
                title: L10n.text("state.stopping.title", fallback: "Disconnecting"),
                detail: L10n.text("state.stopping.short_detail", fallback: "Stopping VPN"),
                isLoading: true
            )
        case .disconnected, .invalid:
            if credentials == nil && ["privacy-consent", "redeeming"].contains(state.phase) {
                return
            }
            let wasStarting = ["redeeming", "awaiting-permission", "connecting"].contains(state.phase)
            if let error = tunnel.lastDisconnectError {
                clearSession()
                showDisconnectError(error, deviceName: deviceName)
            } else if credentials != nil {
                recoverDisconnectFailure(deviceName: deviceName)
            } else if wasStarting {
                showDisconnectError(
                    L10n.text(
                        "state.ios_stopped",
                        fallback: "iOS stopped the tunnel without returning further details."
                    ),
                    deviceName: deviceName
                )
            } else if state.phase != "failed" {
                state = stoppedState()
            }
        @unknown default: break
        }
    }

    private func refreshDiagnostics() async {
        guard !tunnel.isUserRequestedStop,
              tunnel.status == .connected || tunnel.status == .reasserting else {
            if tunnel.status == .disconnected || tunnel.status == .invalid { diagnostics = nil }
            return
        }
        guard let value = await tunnel.diagnostics() else { return }
        guard !tunnel.isUserRequestedStop,
              tunnel.status == .connected || tunnel.status == .reasserting else { return }
        diagnostics = value
        let deviceName = credentials?.deviceName ?? state.deviceName
        if value.runtimeConnected {
            hasObservedRuntimeConnection = true
            state = connectedState(deviceName: deviceName)
        } else if hasObservedRuntimeConnection {
            state = connectedState(deviceName: deviceName)
        } else if tunnel.status == .connected {
            state = checkingState(deviceName: deviceName, detail: value.lastError)
        }
    }

    func refreshFromTunnelError(_ error: String?) {
        guard !tunnel.isUserRequestedStop, let error, !error.isEmpty else { return }
        let deviceName = credentials?.deviceName ?? state.deviceName
        disconnectRecoveryTask?.cancel()
        disconnectRecoveryTask = nil
        clearSession()
        showDisconnectError(error, deviceName: deviceName)
    }

    private func recoverDisconnectFailure(deviceName: String) {
        guard disconnectRecoveryTask == nil, let current = credentials else { return }
        state = State(
            phase: "recovering-error",
            title: L10n.text("state.failed.title", fallback: "VPN connection failed"),
            detail: L10n.text(
                "state.recovering.detail",
                fallback: "Retrieving the reason for the interruption"
            ),
            deviceName: deviceName,
            isLoading: true
        )
        disconnectRecoveryTask = Task { [weak self] in
            guard let self else { return }
            defer { disconnectRecoveryTask = nil }
            for attempt in 0..<6 {
                guard !Task.isCancelled,
                      credentials?.sessionId == current.sessionId,
                      tunnel.status == .disconnected || tunnel.status == .invalid else { return }
                if let nativeError = tunnel.lastDisconnectError, !nativeError.isEmpty {
                    clearSession()
                    showDisconnectError(nativeError, deviceName: deviceName)
                    return
                }
                if let session = try? await api.session(current), session.status == .failed {
                    let message = session.errorMessage?.trimmingCharacters(in: .whitespacesAndNewlines)
                    let code = session.errorCode?.trimmingCharacters(in: .whitespacesAndNewlines)
                    let detail = [message, code].compactMap { value in
                        value?.isEmpty == false ? value : nil
                    }.joined(separator: "\n")
                    clearSession()
                    showDisconnectError(
                        detail.isEmpty
                            ? L10n.text(
                                "state.runtime_stopped",
                                fallback: "The runtime ended the VPN session."
                            )
                            : detail,
                        deviceName: deviceName
                    )
                    return
                }
                if attempt < 5 { try? await Task.sleep(for: .milliseconds(500)) }
            }
            guard !Task.isCancelled,
                  credentials?.sessionId == current.sessionId,
                  tunnel.status == .disconnected || tunnel.status == .invalid else { return }
            clearSession()
            showDisconnectError(
                L10n.text(
                    "state.ios_stopped",
                    fallback: "iOS stopped the tunnel without returning further details."
                ),
                deviceName: deviceName
            )
        }
    }

    private func clearSession() {
        pendingTicket = nil
        credentials = nil
        store.clear()
        diagnostics = nil
        hasObservedRuntimeConnection = false
    }

    private func completeDisconnect(_ current: SessionCredentials?, deviceName: String) async {
        defer { disconnectTask = nil }
        guard let current else {
            clearSession()
            state = stoppedState()
            return
        }

        var lastError: Error?
        for attempt in 0..<3 {
            guard !Task.isCancelled else { return }
            do {
                try await api.stop(current)
                clearSession()
                state = State(
                    phase: "stopped",
                    title: L10n.text("state.stopped.title", fallback: "VPN disconnected"),
                    detail: L10n.text(
                        "state.stopped.portal_detail",
                        fallback: "The session has also been closed in the portal."
                    )
                )
                return
            } catch {
                lastError = error
                if attempt < 2 {
                    try? await Task.sleep(for: .milliseconds(350 * (attempt + 1)))
                }
            }
        }

        guard !Task.isCancelled else { return }
        state = State(
            phase: "stop-failed",
            title: L10n.text("state.stop_failed.title", fallback: "Disconnection not confirmed"),
            detail: L10n.text(
                "state.stop_failed.detail",
                fallback: "The local VPN was stopped, but the portal did not confirm the closure. Try again."
            ),
            deviceName: deviceName,
            error: lastError?.localizedDescription
        )
    }

    private func showDisconnectError(_ error: String, deviceName: String) {
        state = State(
            phase: "failed",
            title: L10n.text("state.failed.title", fallback: "VPN connection failed"),
            detail: L10n.text(
                "state.failed.detail",
                fallback: "Check the details below, then try again from the portal."
            ),
            deviceName: deviceName,
            error: error
        )
    }

    private func redeem(_ ticket: String) async {
        var claimedCredentials: SessionCredentials? = nil
        do {
            let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.1.0"
            let (_, newCredentials) = try await api.redeem(ticket: ticket, appVersion: version,
                installationId: store.installationId, portalReturnUrl: portalReturnURL?.absoluteString)
            claimedCredentials = newCredentials
            try store.save(newCredentials)
            credentials = newCredentials
            if let returnUrl = validatedPortalReturnURL(newCredentials.portalReturnUrl) {
                portalReturnURL = returnUrl
            }
            _ = try await api.update(newCredentials, status: .awaitingPermission)
            state = State(
                phase: "awaiting-permission",
                title: L10n.text("state.permission.title", fallback: "Authorize VPN"),
                detail: L10n.text(
                    "state.permission.detail",
                    fallback: "iOS will show the system consent before connecting the device."
                ),
                deviceName: newCredentials.deviceName,
                isLoading: true
            )
            try await tunnel.installAndStart(credentials: newCredentials)
            refreshFromTunnelStatus()
        } catch {
            if let claimedCredentials {
                _ = try? await api.update(claimedCredentials, status: .failed,
                    errorCode: "ERR_MOBILE_VPN_IOS_PERMISSION", errorMessage: error.localizedDescription)
            }
            credentials = nil
            store.clear()
            state = State(
                phase: "failed",
                title: L10n.text("state.failed.title", fallback: "VPN connection failed"),
                detail: L10n.text(
                    "state.failed.message_detail",
                    fallback: "Check the message below, then try again from the portal."
                ),
                error: error.localizedDescription
            )
        }
    }

    private func idleState() -> State {
        State(
            phase: "idle",
            title: L10n.text("state.idle.title", fallback: "Open a VPN from HT Portal"),
            detail: L10n.text(
                "state.idle.detail",
                fallback: "Choose VPN on a device in the portal, then continue in this app."
            )
        )
    }

    private func stoppedState() -> State {
        State(
            phase: "stopped",
            title: L10n.text("state.stopped.title", fallback: "VPN disconnected"),
            detail: L10n.text(
                "state.stopped.detail",
                fallback: "You can start a new VPN from the portal."
            )
        )
    }

    private func connectedState(deviceName: String) -> State {
        State(
            phase: "connected",
            title: L10n.text("state.connected.title", fallback: "VPN connected"),
            detail: L10n.text(
                "state.connected.detail",
                fallback: "Traffic to remote networks is routed through the secure tunnel."
            ),
            deviceName: deviceName
        )
    }

    private func checkingState(deviceName: String, detail: String? = nil) -> State {
        State(
            phase: "connecting",
            title: L10n.text("state.checking.title", fallback: "Checking connection"),
            detail: detail ?? L10n.text(
                "state.checking.detail",
                fallback: "The tunnel is active; waiting for the runtime to respond."
            ),
            deviceName: deviceName,
            isLoading: true
        )
    }

    private func connectingState(deviceName: String) -> State {
        State(
            phase: "connecting",
            title: L10n.text("state.connecting.title", fallback: "Connecting to device"),
            detail: L10n.text("state.connecting.detail", fallback: "Preparing the secure tunnel"),
            deviceName: deviceName,
            isLoading: true
        )
    }

#if DEBUG
    private func showStorePreview(_ mode: String) {
        let deviceName = L10n.text("preview.device", fallback: "Packaging Line 04")
        switch mode {
        case "connecting":
            state = checkingState(deviceName: deviceName)
        case "connected", "diagnostics":
            state = connectedState(deviceName: deviceName)
            diagnostics = TunnelDiagnostics(
                phase: "connected",
                transport: "udp-p2p",
                assignedAddress: "10.253.253.11",
                runtimeAddress: "10.253.253.10:3000",
                includedRoutes: ["10.13.0.0/16", "192.168.40.0/24"],
                skippedRoutes: [],
                packetSummary: "tx=128, txErrors=0, rx=126, injected=126, injectErrors=0",
                runtimeConnected: true,
                lastError: nil
            )
        default:
            state = idleState()
        }
    }
#endif
}
