import Foundation
import StoreKit
import SwiftUI

enum ProFeature: String, CaseIterable {
    case premiumThemes
    case layoutImport
    case layoutExport
    case customTagColors
    case customContainerQuota
    case unlimitedNotes
    case persistentAppSorting
    case customHotkeys

    var titleKey: String {
        switch self {
        case .premiumThemes:
            return "pro.feature.themes"
        case .layoutImport:
            return "pro.feature.import"
        case .layoutExport:
            return "pro.feature.export"
        case .customTagColors:
            return "pro.feature.customTagColors"
        case .customContainerQuota:
            return "pro.feature.customContainerQuota"
        case .unlimitedNotes:
            return "pro.feature.notes"
        case .persistentAppSorting:
            return "pro.feature.sorting"
        case .customHotkeys:
            return "pro.feature.customHotkeys"
        }
    }

    var benefitKey: String {
        switch self {
        case .premiumThemes:
            return "pro.feature.themes.benefit"
        case .layoutImport:
            return "pro.feature.import.benefit"
        case .layoutExport:
            return "pro.feature.export.benefit"
        case .customTagColors:
            return "pro.feature.customTagColors.benefit"
        case .customContainerQuota:
            return "pro.feature.customContainerQuota.benefit"
        case .unlimitedNotes:
            return "pro.feature.notes.benefit"
        case .persistentAppSorting:
            return "pro.feature.sorting.benefit"
        case .customHotkeys:
            return "pro.feature.customHotkeys.benefit"
        }
    }
}

enum ProAccessState: String, Codable, Equatable {
    case free
    case purchased
    case legacy

    var isUnlocked: Bool {
        self != .free
    }
}

enum ProOperationState: Equatable {
    case idle
    case loading
    case purchasing
    case restoring
    case pending
    case cancelled
    case restoreNotFound
    case failed(messageKey: String)

    var isBusy: Bool {
        switch self {
        case .loading, .purchasing, .restoring:
            return true
        case .idle, .pending, .cancelled, .restoreNotFound, .failed:
            return false
        }
    }
}

enum ProEntitlementError: Error, Equatable {
    case featureLocked(ProFeature)
    case noteQuotaExceeded(ProNoteQuotaStatus)

    var feature: ProFeature? {
        switch self {
        case .featureLocked(let feature):
            return feature
        case .noteQuotaExceeded:
            return .unlimitedNotes
        }
    }

    var noteQuotaStatus: ProNoteQuotaStatus? {
        switch self {
        case .featureLocked:
            return nil
        case .noteQuotaExceeded(let status):
            return status
        }
    }
}

struct ProThemePreviewState: Equatable {
    let theme: AppGridTheme
    let themeTuning: Double?
    let startedAt: Date
    let endsAt: Date

    func remainingSeconds(at date: Date = Date()) -> Int {
        max(0, Int(ceil(endsAt.timeIntervalSince(date))))
    }
}

struct ProDisplayModePreviewState: Equatable {
    let displayMode: String
    let startedAt: Date
    let endsAt: Date

    func remainingSeconds(at date: Date = Date()) -> Int {
        max(0, Int(ceil(endsAt.timeIntervalSince(date))))
    }
}

struct ProNoteQuotaStatus: Equatable {
    let isUnlimited: Bool
    let used: Int
    let limit: Int

    var remaining: Int {
        max(0, limit - used)
    }
}

enum ProNoteSaveDecision: Equatable {
    case allow(ProNoteQuotaStatus)
    case blocked(ProNoteQuotaStatus)
}

struct ProCustomContainerQuotaStatus: Equatable {
    let isUnlimited: Bool
    let used: Int
    let limit: Int

    var remaining: Int {
        max(0, limit - used)
    }
}

enum ProCustomContainerMaintenanceDecision: Equatable {
    case allow(ProCustomContainerQuotaStatus)
    case blocked(ProCustomContainerQuotaStatus)
}

enum ProEntitlementConfig {
    // 发布前只改这里，不允许把商品信息和边界版本散落到业务模块。
    static let lifetimeProductID = "com.taglauncher.pro.lifetime"
    static let firstFreeProVersion = "8.3.2"
    static let legacyFallbackOriginalPurchaseDateISO8601: String? = nil

    static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
    static let freeNoteLimit = 3
    static let freeCustomContainerLimit = 6
    static let defaultThemePreviewDuration: TimeInterval = 5 * 60

    static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"
    static let qaThemePreviewSecondsEnvKey = "TAGLAUNCHER_QA_THEME_PREVIEW_SECONDS"

    static var legacyFallbackOriginalPurchaseDate: Date? {
        guard let value = legacyFallbackOriginalPurchaseDateISO8601 else { return nil }
        return iso8601Formatter.date(from: value)
    }

    static var themePreviewDuration: TimeInterval {
        let environment = ProcessInfo.processInfo.environment
        if let raw = environment[qaThemePreviewSecondsEnvKey],
           let seconds = TimeInterval(raw),
           seconds > 0 {
            return seconds
        }
        return defaultThemePreviewDuration
    }

    private static let iso8601Formatter: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime]
        return formatter
    }()
}

private struct ProEntitlementCache: Codable {
    var accessState: ProAccessState
    var updatedAt: Date
    var originalAppVersion: String?
    var originalPurchaseDate: Date?
}

private struct ProEntitlementSnapshot {
    var accessState: ProAccessState = .free
    var previewThemeID: String? = nil
    var previewThemeTuning: Double? = nil
    var previewDisplayMode: String? = nil
}

private enum ProEntitlementSnapshotStore {
    private static let lock = NSLock()
    private static var snapshot = ProEntitlementSnapshot()

    static func update(_ block: (inout ProEntitlementSnapshot) -> Void) {
        lock.lock()
        block(&snapshot)
        lock.unlock()
    }

    static func read() -> ProEntitlementSnapshot {
        lock.lock()
        let value = snapshot
        lock.unlock()
        return value
    }
}

enum ProEntitlementPolicy {
    static let premiumDisplayModes: Set<String> = ["coloredContainer", "coloredGridContainer"]

    static func accessState() -> ProAccessState {
        ProEntitlementSnapshotStore.read().accessState
    }

    static func lockedErrorIfNeeded(for feature: ProFeature) -> ProEntitlementError? {
        isUnlocked(feature) ? nil : .featureLocked(feature)
    }

    static func requireUnlocked(_ feature: ProFeature) throws {
        if let error = lockedErrorIfNeeded(for: feature) {
            throw error
        }
    }

    static func isUnlocked(_ feature: ProFeature) -> Bool {
        switch feature {
        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
            return accessState().isUnlocked
        }
    }

    static func canUseTheme(_ theme: AppGridTheme) -> Bool {
        ProEntitlementConfig.freeThemes.contains(theme) || isUnlocked(.premiumThemes)
    }

    static func isPremiumDisplayMode(_ displayMode: String) -> Bool {
        premiumDisplayModes.contains(displayMode)
    }

    static func canUseDisplayMode(_ displayMode: String) -> Bool {
        !isPremiumDisplayMode(displayMode) || isUnlocked(.premiumThemes)
    }

    static func fallbackDisplayMode(for displayMode: String) -> String {
        switch displayMode {
        case "coloredContainer":
            return "container"
        case "coloredGridContainer":
            return "gridContainer"
        default:
            return displayMode
        }
    }

    static func effectiveDisplayMode(for storedDisplayMode: String) -> String {
        let snapshot = ProEntitlementSnapshotStore.read()
        if let previewDisplayMode = snapshot.previewDisplayMode,
           isPremiumDisplayMode(previewDisplayMode) {
            return previewDisplayMode
        }
        guard canUseDisplayMode(storedDisplayMode) else {
            return fallbackDisplayMode(for: storedDisplayMode)
        }
        return storedDisplayMode
    }

    static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme {
        if let previewID = ProEntitlementSnapshotStore.read().previewThemeID,
           let previewTheme = AppGridTheme(rawValue: previewID) {
            return previewTheme
        }
        guard canUseTheme(storedTheme) else {
            return .defaultLight
        }
        return storedTheme
    }

    static func effectiveThemeTuning(for storedTheme: AppGridTheme, storedTuning: Double?) -> Double? {
        let snapshot = ProEntitlementSnapshotStore.read()
        if let previewID = snapshot.previewThemeID,
           let previewTheme = AppGridTheme(rawValue: previewID),
           previewTheme.supportsColorTuning {
            return previewTheme.normalizedTuning(snapshot.previewThemeTuning)
        }
        guard canUseTheme(storedTheme), storedTheme.supportsColorTuning else {
            return nil
        }
        return storedTheme.normalizedTuning(storedTuning)
    }

    static func noteQuotaStatus(in store: TagDatabase.Store? = nil) -> ProNoteQuotaStatus {
        guard !isUnlocked(.unlimitedNotes) else {
            return ProNoteQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
        }
        let used = TagDatabase.manualNonEmptyNoteCount(in: store)
        return ProNoteQuotaStatus(
            isUnlimited: false,
            used: used,
            limit: ProEntitlementConfig.freeNoteLimit
        )
    }

    static func noteSaveDecision(note: String, for path: String, in store: TagDatabase.Store? = nil) -> ProNoteSaveDecision {
        let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
        let effectiveStore = store ?? TagDatabase.load()
        let currentQuota = noteQuotaStatus(in: effectiveStore)
        guard !currentQuota.isUnlimited else { return .allow(currentQuota) }

        let manualNotePaths = TagDatabase.manualNonEmptyNotePaths(in: effectiveStore)
        let hasExistingManualNote = manualNotePaths.contains(path)
        let willPersistManualNote = !trimmed.isEmpty
        let limit = ProEntitlementConfig.freeNoteLimit
        let resultingUsed: Int

        switch (hasExistingManualNote, willPersistManualNote) {
        case (true, true):
            resultingUsed = manualNotePaths.count
        case (true, false):
            resultingUsed = max(0, manualNotePaths.count - 1)
        case (false, true):
            guard manualNotePaths.count < limit else {
                return .blocked(currentQuota)
            }
            resultingUsed = manualNotePaths.count + 1
        case (false, false):
            resultingUsed = manualNotePaths.count
        }

        return .allow(
            ProNoteQuotaStatus(
                isUnlimited: false,
                used: resultingUsed,
                limit: limit
            )
        )
    }

    static func requireNoteSave(note: String, for path: String, in store: TagDatabase.Store? = nil) throws -> ProNoteQuotaStatus {
        switch noteSaveDecision(note: note, for: path, in: store) {
        case .allow(let status):
            return status
        case .blocked(let status):
            throw ProEntitlementError.noteQuotaExceeded(status)
        }
    }

    static func customContainerQuotaStatus(in store: TagDatabase.Store? = nil) -> ProCustomContainerQuotaStatus {
        guard !isUnlocked(.customContainerQuota) else {
            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
        }
        let used = TagDatabase.customMaintainedContainerCount(in: store)
        return ProCustomContainerQuotaStatus(
            isUnlimited: false,
            used: used,
            limit: ProEntitlementConfig.freeCustomContainerLimit
        )
    }

    static func customContainerMaintenanceDecision(
        addingCustomContainerCount addedCount: Int,
        in store: TagDatabase.Store? = nil
    ) -> ProCustomContainerMaintenanceDecision {
        let status = customContainerQuotaStatus(in: store)
        guard !status.isUnlimited else { return .allow(status) }
        guard addedCount > 0 else { return .allow(status) }
        guard status.used + addedCount <= status.limit else {
            return .blocked(status)
        }
        return .allow(
            ProCustomContainerQuotaStatus(
                isUnlimited: false,
                used: status.used + addedCount,
                limit: status.limit
            )
        )
    }
}

@MainActor
final class ProEntitlementCenter: ObservableObject {
    static let shared = ProEntitlementCenter()

    @Published private(set) var accessState: ProAccessState = .free
    @Published private(set) var operationState: ProOperationState = .idle
    @Published private(set) var priceDisplayText: String? = nil
    @Published private(set) var themePreviewState: ProThemePreviewState? = nil
    @Published private(set) var displayModePreviewState: ProDisplayModePreviewState? = nil

    private var hasStarted = false
    private var cachedProduct: Product?
    private var updateTask: Task<Void, Never>?
    private var themePreviewTask: Task<Void, Never>?
    private var displayModePreviewTask: Task<Void, Never>?

    private init() {}

    deinit {
        updateTask?.cancel()
        themePreviewTask?.cancel()
        displayModePreviewTask?.cancel()
    }

    var isUnlocked: Bool {
        accessState.isUnlocked
    }

    var isPreviewingTheme: Bool {
        themePreviewState != nil
    }

    var isPreviewingProAppearance: Bool {
        themePreviewState != nil || displayModePreviewState != nil
    }

    var compactStatusTextKey: String {
        if isPreviewingProAppearance {
            return "pro.theme.preview.status"
        }
        switch accessState {
        case .free:
            return "pro.status.free"
        case .purchased:
            return "pro.status.unlocked"
        case .legacy:
            return "pro.status.legacyUnlocked"
        }
    }

    func start() {
        guard !hasStarted else { return }
        hasStarted = true

        if let override = qaOverrideAccessState {
            applyAccessState(override, persistCache: false)
            operationState = .idle
            return
        }

        if let cached = loadCache() {
            applyAccessState(cached.accessState, persistCache: false)
        }

        operationState = .loading
        startTransactionUpdates()

        Task {
            _ = try? await loadProductIfNeeded()
            await refreshEntitlements()
        }
    }

    func purchasePro() {
        Task {
            operationState = .purchasing
            do {
                guard let product = try await loadProductIfNeeded() else {
                    operationState = .failed(messageKey: "pro.purchase.unavailable")
                    return
                }
                let result = try await product.purchase()
                switch result {
                case .success(let verification):
                    switch verification {
                    case .verified(let transaction):
                        await transaction.finish()
                        await refreshEntitlements()
                        operationState = .idle
                    case .unverified:
                        operationState = .failed(messageKey: "pro.purchase.verificationFailed")
                    }
                case .pending:
                    operationState = .pending
                case .userCancelled:
                    operationState = .cancelled
                @unknown default:
                    operationState = .failed(messageKey: "pro.purchase.failed")
                }
            } catch {
                operationState = .failed(messageKey: "pro.purchase.failed")
            }
        }
    }

    func restorePurchases() {
        Task {
            operationState = .restoring
            do {
                try await AppStore.sync()
                await refreshEntitlements()
                if accessState.isUnlocked {
                    operationState = .idle
                } else {
                    operationState = .restoreNotFound
                }
            } catch {
                operationState = .failed(messageKey: "pro.restore.failed")
            }
        }
    }

    func clearTransientOperationState() {
        switch operationState {
        case .pending, .cancelled, .restoreNotFound, .failed:
            operationState = .idle
        case .idle, .loading, .purchasing, .restoring:
            break
        }
    }

    func startThemePreview(for theme: AppGridTheme, tuning: Double? = nil) {
        guard !ProEntitlementConfig.freeThemes.contains(theme) else { return }
        guard !accessState.isUnlocked else { return }

        stopDisplayModePreview()
        themePreviewTask?.cancel()

        let preview = ProThemePreviewState(
            theme: theme,
            themeTuning: theme.supportsColorTuning ? theme.normalizedTuning(tuning) : nil,
            startedAt: Date(),
            endsAt: Date().addingTimeInterval(ProEntitlementConfig.themePreviewDuration)
        )
        applyThemePreview(preview)

        themePreviewTask = Task { [weak self] in
            let duration = max(1, ProEntitlementConfig.themePreviewDuration)
            try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
            guard !Task.isCancelled else { return }
            self?.stopThemePreview()
        }
    }

    func updateThemePreviewTuning(_ tuning: Double, for theme: AppGridTheme) {
        guard let preview = themePreviewState,
              preview.theme == theme,
              theme.supportsColorTuning,
              !accessState.isUnlocked
        else { return }
        let updatedPreview = ProThemePreviewState(
            theme: preview.theme,
            themeTuning: theme.normalizedTuning(tuning),
            startedAt: preview.startedAt,
            endsAt: preview.endsAt
        )
        applyThemePreview(updatedPreview)
    }

    func stopThemePreview() {
        themePreviewTask?.cancel()
        themePreviewTask = nil
        applyThemePreview(nil)
    }

    func startDisplayModePreview(for displayMode: String) {
        guard ProEntitlementPolicy.isPremiumDisplayMode(displayMode) else { return }
        guard !accessState.isUnlocked else { return }

        stopThemePreview()
        displayModePreviewTask?.cancel()

        let now = Date()
        let preview = ProDisplayModePreviewState(
            displayMode: displayMode,
            startedAt: now,
            endsAt: now.addingTimeInterval(ProEntitlementConfig.themePreviewDuration)
        )
        applyDisplayModePreview(preview)

        displayModePreviewTask = Task { [weak self] in
            let duration = max(1, ProEntitlementConfig.themePreviewDuration)
            try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
            guard !Task.isCancelled else { return }
            self?.stopDisplayModePreview()
        }
    }

    func stopDisplayModePreview() {
        displayModePreviewTask?.cancel()
        displayModePreviewTask = nil
        applyDisplayModePreview(nil)
    }

    private func startTransactionUpdates() {
        guard updateTask == nil else { return }
        updateTask = Task { [weak self] in
            for await update in Transaction.updates {
                guard let self else { return }
                if case .verified(let transaction) = update,
                   transaction.productID == ProEntitlementConfig.lifetimeProductID {
                    await transaction.finish()
                }
                await self.refreshEntitlements()
            }
        }
    }

    @discardableResult
    private func loadProductIfNeeded() async throws -> Product? {
        if let cachedProduct {
            return cachedProduct
        }
        let products = try await Product.products(for: [ProEntitlementConfig.lifetimeProductID])
        let product = products.first
        cachedProduct = product
        priceDisplayText = product?.displayPrice
        return product
    }

    private func refreshEntitlements() async {
        if let override = qaOverrideAccessState {
            applyAccessState(override, persistCache: false)
            operationState = .idle
            return
        }

        let purchased = await hasPurchasedLifetimePro()
        let appTransaction = await verifiedAppTransaction()
        let legacy = appTransaction.map(isLegacyPaidUser(appTransaction:)) ?? false

        let state: ProAccessState
        if purchased {
            state = .purchased
        } else if legacy {
            state = .legacy
        } else {
            state = .free
        }

        applyAccessState(
            state,
            persistCache: true,
            originalAppVersion: appTransaction?.originalAppVersion,
            originalPurchaseDate: appTransaction?.originalPurchaseDate
        )

        if case .loading = operationState {
            operationState = .idle
        }
    }

    private func hasPurchasedLifetimePro() async -> Bool {
        for await verification in Transaction.currentEntitlements {
            switch verification {
            case .verified(let transaction):
                if transaction.productID == ProEntitlementConfig.lifetimeProductID,
                   transaction.revocationDate == nil {
                    return true
                }
            case .unverified:
                continue
            }
        }
        return false
    }

    private func verifiedAppTransaction() async -> AppTransaction? {
        do {
            let verification = try await AppTransaction.shared
            switch verification {
            case .verified(let transaction):
                if #available(macOS 13.0, *) {
                    if transaction.revocationDate != nil {
                        return nil
                    }
                }
                return transaction
            case .unverified:
                return nil
            }
        } catch {
            return nil
        }
    }

    private func isLegacyPaidUser(appTransaction: AppTransaction) -> Bool {
        if let current = SemanticVersion(appTransaction.originalAppVersion),
           let cutoff = SemanticVersion(ProEntitlementConfig.firstFreeProVersion) {
            return current < cutoff
        }

        guard let fallbackDate = ProEntitlementConfig.legacyFallbackOriginalPurchaseDate else {
            return false
        }
        return appTransaction.originalPurchaseDate < fallbackDate
    }

    private func applyAccessState(
        _ state: ProAccessState,
        persistCache: Bool,
        originalAppVersion: String? = nil,
        originalPurchaseDate: Date? = nil
    ) {
        let previousState = accessState
        accessState = state
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.accessState = state
        }

        if previousState != state {
            NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged, object: nil)
        }

        if persistCache {
            writeCache(
                ProEntitlementCache(
                    accessState: state,
                    updatedAt: Date(),
                    originalAppVersion: originalAppVersion,
                    originalPurchaseDate: originalPurchaseDate
                )
            )
        }
    }

    private func applyThemePreview(_ preview: ProThemePreviewState?) {
        themePreviewState = preview
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.previewThemeID = preview?.theme.rawValue
            snapshot.previewThemeTuning = preview?.themeTuning
        }
    }

    private func applyDisplayModePreview(_ preview: ProDisplayModePreviewState?) {
        displayModePreviewState = preview
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.previewDisplayMode = preview?.displayMode
        }
    }

    private var cacheURL: URL {
        AppIdentity.applicationSupportDirectory
            .appendingPathComponent("pro-entitlement-cache.json")
    }

    private func loadCache() -> ProEntitlementCache? {
        guard let data = try? Data(contentsOf: cacheURL) else { return nil }
        return try? JSONDecoder().decode(ProEntitlementCache.self, from: data)
    }

    private func writeCache(_ cache: ProEntitlementCache) {
        try? FileManager.default.createDirectory(
            at: AppIdentity.applicationSupportDirectory,
            withIntermediateDirectories: true,
            attributes: nil
        )
        guard let data = try? JSONEncoder().encode(cache) else { return }
        try? data.write(to: cacheURL, options: .atomic)
    }

    private var qaOverrideAccessState: ProAccessState? {
        guard let raw = ProcessInfo.processInfo.environment[ProEntitlementConfig.qaStateEnvKey] else {
            return nil
        }
        switch raw {
        case "free":
            return .free
        case "pro":
            return .purchased
        case "legacyPro":
            return .legacy
        default:
            return nil
        }
    }
}

private struct SemanticVersion: Comparable {
    private let components: [Int]

    init?(_ raw: String) {
        let values = raw
            .split(separator: ".")
            .compactMap { Int($0.trimmingCharacters(in: .whitespacesAndNewlines)) }
        guard !values.isEmpty else { return nil }
        components = values
    }

    static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
        let maxCount = max(lhs.components.count, rhs.components.count)
        for index in 0..<maxCount {
            let left = index < lhs.components.count ? lhs.components[index] : 0
            let right = index < rhs.components.count ? rhs.components[index] : 0
            if left != right {
                return left < right
            }
        }
        return false
    }
}

extension TagDatabase {
    static func manualNonEmptyNotePaths(in store: Store) -> Set<String> {
        Set(
            store.appNotes.compactMap { path, note in
                let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !trimmed.isEmpty,
                      store.appNoteMetadata[path]?.origin == .manual
                else { return nil }
                return path
            }
        )
    }

    static func manualNonEmptyNoteCount(in store: Store? = nil) -> Int {
        let effectiveStore = store ?? load()
        return manualNonEmptyNotePaths(in: effectiveStore).count
    }
}
