//
//  BackupService.swift
//  oAI
//
//  iCloud Drive backup of non-encrypted settings (Option C, v1)
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// oAI is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
// Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.

import Foundation
import os

// MARK: - BackupManifest

struct BackupManifest: Codable {
    let version: Int
    let createdAt: String
    let appVersion: String
    let credentialsIncluded: Bool
    let settings: [String: String]
    let credentials: [String: String]?
}

// MARK: - BackupService

@Observable
final class BackupService {
    static let shared = BackupService()

    private let log = Logger(subsystem: "oAI", category: "backup")

    /// Whether iCloud Drive is available on this machine
    var iCloudAvailable: Bool = false

    /// Date of the last backup file on disk (from file attributes)
    var lastBackupDate: Date?

    /// URL of the last backup file
    var lastBackupURL: URL?

    // Keys excluded from backup — encrypted_ prefix + internal migration flags
    private static let excludedKeys: Set<String> = [
        "encrypted_openrouterAPIKey",
        "encrypted_anthropicAPIKey",
        "encrypted_openaiAPIKey",
        "encrypted_googleAPIKey",
        "encrypted_googleSearchEngineID",
        "encrypted_anytypeMcpAPIKey",
        "encrypted_paperlessAPIToken",
        "encrypted_syncUsername",
        "encrypted_syncPassword",
        "encrypted_syncAccessToken",
        "encrypted_emailUsername",
        "encrypted_emailPassword",
        "_migrated",
        "_keychain_migrated",
    ]

    private init() {
        checkForExistingBackup()
    }

    // MARK: - iCloud Path Resolution

    private func resolveBackupDirectory() -> URL {
        let home = FileManager.default.homeDirectoryForCurrentUser
        let icloudRoot = home.appendingPathComponent("Library/Mobile Documents/com~apple~CloudDocs")
        if FileManager.default.fileExists(atPath: icloudRoot.path) {
            let icloudOAI = icloudRoot.appendingPathComponent("oAI")
            try? FileManager.default.createDirectory(at: icloudOAI, withIntermediateDirectories: true)
            return icloudOAI
        }
        // Fallback: Downloads
        return FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
            ?? FileManager.default.temporaryDirectory
    }

    func checkForExistingBackup() {
        let home = FileManager.default.homeDirectoryForCurrentUser
        let icloudRoot = home.appendingPathComponent("Library/Mobile Documents/com~apple~CloudDocs")
        iCloudAvailable = FileManager.default.fileExists(atPath: icloudRoot.path)

        let dir = resolveBackupDirectory()
        let fileURL = dir.appendingPathComponent("oai_backup.json")
        if FileManager.default.fileExists(atPath: fileURL.path),
           let attrs = try? FileManager.default.attributesOfItem(atPath: fileURL.path),
           let modified = attrs[.modificationDate] as? Date {
            lastBackupDate = modified
            lastBackupURL = fileURL
        }
    }

    // MARK: - Export

    /// Export all non-encrypted settings to iCloud Drive (or Downloads).
    /// Returns the URL where the file was written.
    @discardableResult
    func exportSettings() async throws -> URL {
        // Load raw settings from DB
        guard let allSettings = try? DatabaseService.shared.loadAllSettings() else {
            throw BackupError.databaseReadFailed
        }

        // Filter out excluded keys
        let filtered = allSettings.filter { !Self.excludedKeys.contains($0.key) }

        // Build manifest
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime]
        let manifest = BackupManifest(
            version: 1,
            createdAt: formatter.string(from: Date()),
            appVersion: appVersion(),
            credentialsIncluded: false,
            settings: filtered,
            credentials: nil
        )

        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        let data = try encoder.encode(manifest)

        let dir = resolveBackupDirectory()
        let fileURL = dir.appendingPathComponent("oai_backup.json")
        try data.write(to: fileURL, options: .atomic)

        log.info("Backup written to \(fileURL.path, privacy: .public) (\(filtered.count) settings)")

        await MainActor.run {
            self.lastBackupDate = Date()
            self.lastBackupURL = fileURL
        }

        return fileURL
    }

    // MARK: - Import

    /// Restore settings from a backup JSON file.
    func importSettings(from url: URL) async throws {
        let data = try Data(contentsOf: url)

        let decoder = JSONDecoder()
        let manifest: BackupManifest
        do {
            manifest = try decoder.decode(BackupManifest.self, from: data)
        } catch {
            throw BackupError.invalidFormat(error.localizedDescription)
        }

        guard manifest.version == 1 else {
            throw BackupError.unsupportedVersion(manifest.version)
        }

        // Write each setting to the database
        for (key, value) in manifest.settings {
            DatabaseService.shared.setSetting(key: key, value: value)
        }

        // Refresh in-memory cache
        SettingsService.shared.reloadFromDatabase()

        log.info("Restored \(manifest.settings.count) settings from backup (v\(manifest.version))")
    }

    // MARK: - Helpers

    private func appVersion() -> String {
        Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
    }
}

// MARK: - BackupError

enum BackupError: LocalizedError {
    case databaseReadFailed
    case invalidFormat(String)
    case unsupportedVersion(Int)

    var errorDescription: String? {
        switch self {
        case .databaseReadFailed:
            return "Could not read settings from the database."
        case .invalidFormat(let detail):
            return "The backup file is not valid: \(detail)"
        case .unsupportedVersion(let v):
            return "Backup version \(v) is not supported by this version of oAI."
        }
    }
}
