//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Combine
import Foundation
import UserNotifications

struct OpenRockyTimer: Identifiable, Codable, Sendable, Equatable {
    let id: String
    let label: String
    let createdAt: Date
    let fireAt: Date
}

/// In-app countdown timers. Backed by `UNUserNotificationCenter` so a fired timer
/// reaches the user even if the app is backgrounded or killed.
///
/// For a voice-first flow the AI calls the `timer` tool (see `OpenRockyToolbox`); the
/// Features screen shows active timers so the user can cancel visually.
@MainActor
final class OpenRockyTimerService: ObservableObject {
    static let shared = OpenRockyTimerService()

    @Published private(set) var timers: [OpenRockyTimer] = []

    private let storageKey = "rocky.timers.v1"
    private let defaults = UserDefaults.standard

    private init() {
        load()
        // Drop anything that fired while the app was closed — the notification already ran.
        pruneExpired()
    }

    // MARK: - Public API

    @discardableResult
    func schedule(label: String, duration: TimeInterval) async throws -> OpenRockyTimer {
        guard duration > 0 else { throw OpenRockyTimerError.invalidDuration }
        try await ensureAuthorized()

        let cleanLabel = label.trimmingCharacters(in: .whitespacesAndNewlines)
        let displayLabel = cleanLabel.isEmpty ? "Timer" : cleanLabel
        let timer = OpenRockyTimer(
            id: UUID().uuidString,
            label: displayLabel,
            createdAt: Date(),
            fireAt: Date().addingTimeInterval(duration)
        )

        try await scheduleNotification(for: timer, after: duration)

        timers.append(timer)
        save()
        return timer
    }

    func cancel(id: String) {
        UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
        timers.removeAll { $0.id == id }
        save()
    }

    func cancelAll() {
        let ids = timers.map(\.id)
        UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
        timers.removeAll()
        save()
    }

    func list() -> [OpenRockyTimer] {
        pruneExpired()
        return timers.sorted { $0.fireAt < $1.fireAt }
    }

    // MARK: - Persistence

    private func load() {
        guard let data = defaults.data(forKey: storageKey) else { return }
        if let decoded = try? JSONDecoder.iso().decode([OpenRockyTimer].self, from: data) {
            timers = decoded
        }
    }

    private func save() {
        if let data = try? JSONEncoder.iso().encode(timers) {
            defaults.set(data, forKey: storageKey)
        }
    }

    private func pruneExpired() {
        let now = Date()
        let before = timers.count
        timers.removeAll { $0.fireAt <= now }
        if timers.count != before {
            save()
        }
    }

    // MARK: - Notifications

    private func ensureAuthorized() async throws {
        let center = UNUserNotificationCenter.current()
        let settings = await center.notificationSettings()
        switch settings.authorizationStatus {
        case .authorized, .provisional, .ephemeral:
            return
        case .denied:
            throw OpenRockyTimerError.notificationsDenied
        case .notDetermined:
            let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
            if !granted { throw OpenRockyTimerError.notificationsDenied }
        @unknown default:
            throw OpenRockyTimerError.notificationsDenied
        }
    }

    private func scheduleNotification(for timer: OpenRockyTimer, after duration: TimeInterval) async throws {
        let content = UNMutableNotificationContent()
        content.title = timer.label
        content.body = "Timer finished."
        content.sound = .default
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: max(1, duration), repeats: false)
        let request = UNNotificationRequest(identifier: timer.id, content: content, trigger: trigger)
        try await UNUserNotificationCenter.current().add(request)
    }
}

enum OpenRockyTimerError: LocalizedError {
    case invalidDuration
    case notificationsDenied
    case notFound

    var errorDescription: String? {
        switch self {
        case .invalidDuration: "Timer duration must be greater than 0 seconds."
        case .notificationsDenied: "Rocky needs notification permission to fire timers. Enable it in Settings."
        case .notFound: "Timer not found."
        }
    }
}

private extension JSONEncoder {
    static func iso() -> JSONEncoder {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return encoder
    }
}

private extension JSONDecoder {
    static func iso() -> JSONDecoder {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }
}
