import SwiftUI

struct ComponentLibraryItem: Identifiable, Equatable {
    struct AffiliateLink: Identifiable, Equatable {
        let id: String
        let url: URL
        let country: String?
    }

    let id: String
    let name: String
    let voltageIn: Double?
    let voltageOut: Double?
    let watt: Double?
    let iconURL: URL?
    let affiliateLinks: [AffiliateLink]

    var displayVoltage: Double? {
        voltageIn ?? voltageOut
    }

    var current: Double? {
        guard let power = watt, let voltage = displayVoltage, voltage > 0 else { return nil }
        return power / voltage
    }
    
    var voltageLabel: String? {
        guard let voltage = displayVoltage else { return nil }
        return String(format: "%.1fV", voltage)
    }
    
    var powerLabel: String? {
        guard let power = watt else { return nil }
        return String(format: "%.0fW", power)
    }
    
    var currentLabel: String? {
        guard let current else { return nil }
        return String(format: "%.1fA", current)
    }

    var primaryAffiliateLink: AffiliateLink? {
        affiliateLink(matching: Locale.current.regionCode)
    }

    func affiliateLink(matching regionCode: String?) -> AffiliateLink? {
        guard !affiliateLinks.isEmpty else { return nil }

        let normalizedRegionCode = regionCode?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()

        if let normalizedRegionCode, !normalizedRegionCode.isEmpty {
            if let exactMatch = affiliateLinks.first(where: { link in
                link.country?.lowercased() == normalizedRegionCode
            }) {
                return exactMatch
            }
        }

        if let fallbackWithoutCountry = affiliateLinks.first(where: { $0.country == nil }) {
            return fallbackWithoutCountry
        }

        return affiliateLinks.first
    }
}

@MainActor
final class ComponentLibraryViewModel: ObservableObject {
    @Published private(set) var isLoading = false
    @Published private(set) var items: [ComponentLibraryItem] = []
    @Published private(set) var errorMessage: String?

    private let baseURL = URL(string: "https://base.voltplan.app")!
    private let urlSession: URLSession

    init(urlSession: URLSession = .shared) {
        self.urlSession = urlSession
    }

    func load() async {
        guard !isLoading else { return }
        isLoading = true
        errorMessage = nil

        do {
            let fetchedItems = try await fetchComponents()
            items = fetchedItems
        } catch {
            items = []
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }

    func refresh() async {
        isLoading = false
        await load()
    }

    private func fetchComponents() async throws -> [ComponentLibraryItem] {
        var components = URLComponents(url: baseURL.appendingPathComponent("api/collections/components/records"), resolvingAgainstBaseURL: false)
        components?.queryItems = [
            URLQueryItem(name: "filter", value: "(type='load')"),
            URLQueryItem(name: "sort", value: "+name"),
            URLQueryItem(name: "fields", value: "id,collectionId,name,icon,voltage_in,voltage_out,watt")
        ]

        guard let url = components?.url else {
            throw URLError(.badURL)
        }

        var request = URLRequest(url: url)
        request.cachePolicy = .reloadIgnoringLocalCacheData

        let (data, response) = try await urlSession.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        let decoded = try JSONDecoder().decode(PocketBaseResponse.self, from: data)
        let affiliateLinksByComponent = try await fetchAffiliateLinks(for: decoded.items.map(\.id))
        let mappedItems = decoded.items.map { record in
            ComponentLibraryItem(
                id: record.id,
                name: record.name,
                voltageIn: record.voltageIn,
                voltageOut: record.voltageOut,
                watt: record.watt,
                iconURL: iconURL(for: record),
                affiliateLinks: affiliateLinksByComponent[record.id] ?? []
            )
        }
        for item in mappedItems {
            if let url = item.iconURL {
                Task.detached(priority: .background) {
                    await IconCache.shared.prefetch(url)
                }
            }
        }
        return mappedItems
    }

    private func fetchAffiliateLinks(for componentIDs: [String]) async throws -> [String: [ComponentLibraryItem.AffiliateLink]] {
        let uniqueIDs = Array(Set(componentIDs))
        guard !uniqueIDs.isEmpty else { return [:] }

        let idSet = Set(uniqueIDs)
        let perPage = 200
        let chunkSize = 15
        let chunks: [[String]] = stride(from: 0, to: uniqueIDs.count, by: chunkSize).map { index in
            let upperBound = min(index + chunkSize, uniqueIDs.count)
            return Array(uniqueIDs[index..<upperBound])
        }
        var aggregated: [String: [ComponentLibraryItem.AffiliateLink]] = [:]

        for chunk in chunks {
            guard !chunk.isEmpty else { continue }

            let filterValue = chunk
                .map { "component='\(escapeFilterValue($0))'" }
                .joined(separator: " || ")

            var page = 1

            while true {
                var components = URLComponents(
                    url: baseURL.appendingPathComponent("api/collections/affiliate_links/records"),
                    resolvingAgainstBaseURL: false
                )

                var queryItems = [
                    URLQueryItem(name: "page", value: "\(page)"),
                    URLQueryItem(name: "perPage", value: "\(perPage)"),
                    URLQueryItem(name: "fields", value: "id,url,component,country")
                ]

                if !filterValue.isEmpty {
                    queryItems.append(URLQueryItem(name: "filter", value: "(\(filterValue))"))
                }

                components?.queryItems = queryItems

                guard let url = components?.url else {
                    throw URLError(.badURL)
                }

                var request = URLRequest(url: url)
                request.cachePolicy = .reloadIgnoringLocalCacheData

                let (data, response) = try await urlSession.data(for: request)

                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }

                let decoded = try JSONDecoder().decode(AffiliateLinksResponse.self, from: data)

                for record in decoded.items {
                    guard let componentID = record.component, idSet.contains(componentID) else { continue }
                    guard let url = URL(string: record.url) else { continue }

                    let normalizedCountry = record.country?
                        .trimmingCharacters(in: .whitespacesAndNewlines)
                    let countryCode = normalizedCountry?.isEmpty == true ? nil : normalizedCountry?.uppercased()

                    let link = ComponentLibraryItem.AffiliateLink(
                        id: record.id,
                        url: url,
                        country: countryCode
                    )

                    var links = aggregated[componentID, default: []]
                    if !links.contains(where: { $0.id == record.id }) {
                        links.append(link)
                        aggregated[componentID] = links
                    }
                }

                let isLastPage: Bool
                if decoded.totalPages > 0 {
                    isLastPage = page >= decoded.totalPages
                } else {
                    isLastPage = decoded.items.count < perPage
                }

                if isLastPage { break }
                page += 1
            }
        }

        for key in Array(aggregated.keys) {
            aggregated[key]?.sort { lhs, rhs in
                let lhsCountry = lhs.country ?? ""
                let rhsCountry = rhs.country ?? ""

                if lhsCountry == rhsCountry {
                    return lhs.url.absoluteString < rhs.url.absoluteString
                }

                return lhsCountry < rhsCountry
            }
        }

        return aggregated
    }

    private func iconURL(for record: PocketBaseRecord) -> URL? {
        guard let icon = record.icon else { return nil }

        return baseURL
            .appendingPathComponent("api")
            .appendingPathComponent("files")
            .appendingPathComponent(record.collectionId)
            .appendingPathComponent(record.id)
            .appendingPathComponent(icon)
    }

    private func escapeFilterValue(_ value: String) -> String {
        value.replacingOccurrences(of: "'", with: "\\'")
    }

    private struct PocketBaseResponse: Decodable {
        let items: [PocketBaseRecord]
    }

    private struct PocketBaseRecord: Decodable {
        let id: String
        let collectionId: String
        let name: String
        let icon: String?
        let voltageIn: Double?
        let voltageOut: Double?
        let watt: Double?

        enum CodingKeys: String, CodingKey {
            case id
            case collectionId
            case name
            case icon
            case voltageIn = "voltage_in"
            case voltageOut = "voltage_out"
            case watt
        }
    }

    private struct AffiliateLinksResponse: Decodable {
        let page: Int
        let totalPages: Int
        let items: [AffiliateLinkRecord]
    }

    private struct AffiliateLinkRecord: Decodable {
        let id: String
        let url: String
        let component: String?
        let country: String?
    }
}

struct ComponentLibraryView: View {
    @Environment(\.dismiss) private var dismiss
    @StateObject private var viewModel = ComponentLibraryViewModel()
    @State private var searchText: String = ""
    let onSelect: (ComponentLibraryItem) -> Void
    
    var body: some View {
        NavigationStack {
            content
                .navigationTitle("VoltPlan Library")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("Close") {
                            dismiss()
                        }
                    }
                }
        }
        .task {
            await viewModel.load()
        }
        .refreshable {
            await viewModel.refresh()
        }
        .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search components")
    }

    @ViewBuilder
    private var content: some View {
        if viewModel.isLoading && viewModel.items.isEmpty {
            ProgressView("Loading components")
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
        } else if let errorMessage = viewModel.errorMessage {
            VStack(spacing: 12) {
                Image(systemName: "exclamationmark.triangle.fill")
                    .font(.system(size: 32))
                    .foregroundColor(.orange)
                Text("Unable to load components")
                    .font(.headline)
                Text(errorMessage)
                    .font(.caption)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.center)
                Button("Retry") {
                    Task { await viewModel.refresh() }
                }
                .buttonStyle(.borderedProminent)
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
        } else if filteredItems.isEmpty {
            VStack(spacing: 12) {
                Image(systemName: searchText.isEmpty ? "sparkles.rectangle.stack" : "magnifyingglass")
                    .font(.system(size: 32))
                    .foregroundColor(.secondary)
                Text(searchText.isEmpty ? "No components available" : "No matches")
                    .font(.headline)
                Text(searchText.isEmpty ? "Check back soon for new loads from VoltPlan." : "Try searching for a different name.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
        } else {
            List(filteredItems) { item in
                Button {
                    onSelect(item)
                    dismiss()
                } label: {
                    ComponentRow(item: item)
                }
                .buttonStyle(.plain)
            }
            .listStyle(.insetGrouped)
        }
    }

    private var filteredItems: [ComponentLibraryItem] {
        let trimmedQuery = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedQuery.isEmpty else { return viewModel.items }

        return viewModel.items.filter { item in
            item.name.localizedCaseInsensitiveContains(trimmedQuery)
        }
    }
}

private struct ComponentRow: View {
    let item: ComponentLibraryItem

    var body: some View {
        HStack(spacing: 12) {
            iconView
            VStack(alignment: .leading, spacing: 4) {
                Text(item.name)
                    .font(.headline)
                    .foregroundColor(.primary)
                detailLine
            }
            Spacer()
            Image(systemName: "chevron.right")
                .font(.caption)
                .foregroundColor(Color(.tertiaryLabel))
        }
        .padding(.vertical, 8)
    }

    private var iconView: some View {
        LoadIconView(
            remoteIconURLString: item.iconURL?.absoluteString,
            fallbackSystemName: "bolt",
            fallbackColor: Color.blue.opacity(0.15),
            size: 44
        )
    }

    @ViewBuilder
    private var detailLine: some View {
        let labels = [item.voltageLabel, item.powerLabel, item.currentLabel].compactMap { $0 }

        if labels.isEmpty {
            Text("Details coming soon")
                .font(.caption)
                .foregroundColor(.secondary)
        } else {
            Text(labels.joined(separator: " • "))
                .font(.caption)
                .foregroundColor(.secondary)
        }
    }
}
