import Foundation
import CoreData
import Combine

@MainActor
class CategoryRepository: ObservableObject {
    private let context: NSManagedObjectContext

    @Published private(set) var categories: [Category] = []

    private var cancellables = Set<AnyCancellable>()

    init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
        self.context = context
        fetchCategories()
        ensureDefaultCategoriesExist()
        setupNotificationObserver()
    }

    private func setupNotificationObserver() {
        NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] notification in
                guard let self, self.isRelevantChange(notification) else { return }
                self.fetchCategories()
            }
            .store(in: &cancellables)

        NotificationCenter.default.publisher(for: .cloudKitForceReload)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in self?.fetchCategories() }
            .store(in: &cancellables)
    }

    // MARK: - Fetch

    func fetchCategories() {
        let request: NSFetchRequest<Category> = Category.fetchRequest()
        request.sortDescriptors = [
            NSSortDescriptor(keyPath: \Category.sortOrder, ascending: true),
            NSSortDescriptor(keyPath: \Category.name, ascending: true)
        ]
        // Performance: Add batch size for fetches
        request.fetchBatchSize = 50

        do {
            categories = try context.fetch(request)
        } catch {
            print("Failed to fetch categories: \(error)")
            categories = []
        }
    }

    func fetchCategory(by id: UUID) -> Category? {
        let request: NSFetchRequest<Category> = Category.fetchRequest()
        request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
        request.fetchLimit = 1
        return try? context.fetch(request).first
    }

    // MARK: - Create

    /// Check if a category with the given name already exists (case-insensitive)
    func categoryExists(name: String) -> Bool {
        let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalized = trimmed.lowercased()
        return categories.contains { $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized }
    }

    /// Find an existing category by name (case-insensitive)
    func findCategory(byName name: String) -> Category? {
        let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalized = trimmed.lowercased()
        return categories.first { $0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized }
    }

    @discardableResult
    func createCategory(
        name: String,
        colorHex: String,
        icon: String
    ) -> Category {
        // Check if category with same name already exists
        if let existing = findCategory(byName: name) {
            return existing
        }

        let category = Category(context: context)
        category.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
        category.colorHex = colorHex
        category.icon = icon
        category.sortOrder = Int16(categories.count)

        save()
        return category
    }

    func createDefaultCategoriesIfNeeded() {
        ensureDefaultCategoriesExist()
    }

    func ensureDefaultCategoriesExist() {
        var addedCount = 0
        var nextSortOrder = categories.count

        for categoryData in Category.defaultCategories {
            if categoryExists(name: categoryData.name) {
                continue
            }

            let category = Category(context: context)
            category.name = categoryData.name
            category.colorHex = categoryData.colorHex
            category.icon = categoryData.icon
            category.sortOrder = Int16(nextSortOrder)
            nextSortOrder += 1
            addedCount += 1
        }

        guard addedCount > 0 else { return }
        save()
        fetchCategories()
    }

    // MARK: - Update

    func updateCategory(
        _ category: Category,
        name: String? = nil,
        colorHex: String? = nil,
        icon: String? = nil
    ) {
        if let name = name {
            category.name = name
        }
        if let colorHex = colorHex {
            category.colorHex = colorHex
        }
        if let icon = icon {
            category.icon = icon
        }

        save()
    }

    func updateSortOrder(_ categories: [Category]) {
        for (index, category) in categories.enumerated() {
            category.sortOrder = Int16(index)
        }
        save()
    }

    // MARK: - Delete

    func deleteCategory(_ category: Category) {
        context.delete(category)
        save()
    }

    func deleteCategory(at offsets: IndexSet) {
        for index in offsets {
            guard index < categories.count else { continue }
            context.delete(categories[index])
        }
        save()
    }

    // MARK: - Statistics

    var totalValue: Decimal {
        categories.reduce(Decimal.zero) { $0 + $1.totalValue }
    }

    func categoryAllocation() -> [(category: Category, percentage: Double)] {
        let total = totalValue
        guard total > 0 else { return [] }

        return categories.map { category in
            let percentage = NSDecimalNumber(decimal: category.totalValue / total).doubleValue * 100
            return (category: category, percentage: percentage)
        }.sorted { $0.percentage > $1.percentage }
    }

    // MARK: - Save

    private func save() {
        guard context.hasChanges else { return }
        do {
            try context.save()
            // Note: Removed redundant fetchCategories() call - the NotificationCenter observer
            // in setupNotificationObserver() already handles refetching on context changes
            CoreDataStack.shared.refreshWidgetData()
        } catch {
            print("Failed to save context: \(error)")
        }
    }

    private func isRelevantChange(_ notification: Notification) -> Bool {
        guard let info = notification.userInfo else { return false }
        let keys: [String] = [
            NSInsertedObjectsKey,
            NSUpdatedObjectsKey,
            NSDeletedObjectsKey,
            NSRefreshedObjectsKey
        ]

        for key in keys {
            if let objects = info[key] as? Set<NSManagedObject> {
                if objects.contains(where: { $0 is Category }) {
                    return true
                }
            }
        }
        return false
    }
}
