import ForgejoKit
import SwiftUI

struct MergedSearchableConfig {
    let issueType: String
    let navigationTitle: String
    let searchPrompt: String
    let emptyTitle: String
    let emptyOpenIcon: String
    let emptyClosedIcon: String
    let itemNoun: String
    let createButtonId: String
    let involvementFlags: [InvolvementScope]
}

extension MergedSearchableConfig {
    static let issues = MergedSearchableConfig(
        issueType: "issues",
        navigationTitle: "Issues",
        searchPrompt: "Search issues",
        emptyTitle: "No Issues",
        emptyOpenIcon: "checkmark.circle",
        emptyClosedIcon: "exclamationmark.circle",
        itemNoun: "issues",
        createButtonId: "issue-create-button",
        involvementFlags: InvolvementScope.issueFlags,
    )

    static let pullRequests = MergedSearchableConfig(
        issueType: "pulls",
        navigationTitle: "Pull Requests",
        searchPrompt: "Search pull requests",
        emptyTitle: "No Pull Requests",
        emptyOpenIcon: "arrow.triangle.pull",
        emptyClosedIcon: "arrow.triangle.pull",
        itemNoun: "pull requests",
        createButtonId: "pr-create-button",
        involvementFlags: InvolvementScope.pullRequestFlags,
    )
}

struct MergedSearchableOverviewView<Row: View, Detail: View, CreateView: View>: View {
    let manager: MultiInstanceManager
    let config: MergedSearchableConfig

    @State private var pagination: PaginationState<TaggedItem<Issue>>
    @State private var stateFilter: IssueFilterState = .open
    @State private var searchText = ""
    @State private var searchTask: Task<Void, Never>?
    @State private var showCreateFlow = false

    private let rowContent: (Issue, String) -> Row
    private let detailContent: (Repository, Int, AuthenticationService) -> Detail
    private let createContent: (AuthenticationService, @escaping () -> Void) -> CreateView

    init(
        manager: MultiInstanceManager,
        config: MergedSearchableConfig,
        @ViewBuilder row: @escaping (Issue, String) -> Row,
        @ViewBuilder detail: @escaping (Repository, Int, AuthenticationService) -> Detail,
        @ViewBuilder createView: @escaping (AuthenticationService, @escaping () -> Void) -> CreateView,
    ) {
        self.manager = manager
        self.config = config
        rowContent = row
        detailContent = detail
        createContent = createView

        let state = PaginationState<TaggedItem<Issue>>()
        state.cacheName = "merged_\(config.issueType)"
        state.dedupeKey = { $0.id }
        state.loadFromCache()
        state.rehydrate(from: manager)
        _pagination = State(initialValue: state)
    }

    var body: some View {
        @Bindable var pagination = pagination
        ZStack(alignment: .bottomTrailing) {
            VStack(spacing: 0) {
                if !manager.failedInstances.isEmpty {
                    MergedConnectionWarning(failedInstances: manager.failedInstances)
                }

                List {
                    if pagination.isLoading, pagination.items.isEmpty {
                        LoadingListSection()
                    } else if pagination.items.isEmpty {
                        ContentUnavailableView {
                            Label(
                                config.emptyTitle,
                                systemImage: stateFilter == .open
                                    ? config.emptyOpenIcon : config.emptyClosedIcon,
                            )
                            .foregroundStyle(stateFilter == .open ? .green : .secondary)
                        } description: {
                            Text(
                                stateFilter == .open
                                    ? "All clear \u{2014} no open \(config.itemNoun) to review."
                                    : "No \(stateFilter.rawValue) \(config.itemNoun) found.",
                            )
                        }
                    } else {
                        Section {
                            ForEach(pagination.items) { tagged in
                                if let repository = tagged.item.repository {
                                    NavigationLink {
                                        detailContent(repository, tagged.item.number, tagged.authService)
                                    } label: {
                                        rowContent(tagged.item, tagged.instanceName)
                                    }
                                } else {
                                    rowContent(tagged.item, tagged.instanceName)
                                }
                            }

                            if pagination.hasMore {
                                ProgressView()
                                    .frame(maxWidth: .infinity)
                                    .listRowBackground(Color.clear)
                                    .accessibilityIdentifier("load-more-indicator")
                                    .task {
                                        await loadMore()
                                    }
                            }
                        }
                    }
                }
                .listStyle(.insetGrouped)
            }

            FloatingCreateButton {
                showCreateFlow = true
            }
            .accessibilityIdentifier(config.createButtonId)
        }
        .navigationTitle(config.navigationTitle)
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Menu {
                    Section("State") {
                        filterButton("Open", isSelected: stateFilter == .open) { stateFilter = .open }
                        filterButton("Closed", isSelected: stateFilter == .closed) { stateFilter = .closed }
                        filterButton("All", isSelected: stateFilter == .all) { stateFilter = .all }
                    }
                } label: {
                    Image(
                        systemName: stateFilter != .open
                            ? "line.3.horizontal.decrease.circle.fill"
                            : "line.3.horizontal.decrease.circle",
                    )
                }
                .accessibilityIdentifier("filter-menu-button")
            }
        }
        .searchable(text: $searchText, prompt: config.searchPrompt)
        .refreshable {
            await reloadItems().value
        }
        .task {
            if !pagination.hasLoaded {
                reloadItems()
            }
        }
        .onReceive(
            NotificationCenter.default.publisher(
                for: config.issueType == "pulls" ? .pullRequestsDidChange : .issuesDidChange,
            ),
        ) { _ in
            pagination.invalidate()
            reloadItems()
        }
        .onChange(of: stateFilter) {
            reloadItems(clearItems: true)
        }
        .debouncedSearch(text: $searchText, task: $searchTask) {
            await reloadItems().value
        }
        .sheet(isPresented: $showCreateFlow) {
            MergedCreatePickerView(manager: manager) { _, auth in
                createContent(auth) {
                    showCreateFlow = false
                    reloadItems()
                }
            }
        }
        .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError)
    }

    private func filterButton(
        _ label: String, isSelected: Bool, action: @escaping () -> Void,
    ) -> some View {
        Button(action: action) {
            if isSelected {
                Label(label, systemImage: "checkmark")
            } else {
                Text(label)
            }
        }
    }

    @discardableResult
    private func reloadItems(clearItems: Bool = false) -> Task<Void, Never> {
        pagination.reloadAndCache(clearItems: clearItems) { page, limit in
            try await fetchFromAllInstances(page: page, limit: limit)
        }
    }

    private func loadMore() async {
        await pagination.loadMoreAndCache { page, limit in
            try await fetchFromAllInstances(page: page, limit: limit)
        }
    }

    private func fetchFromAllInstances(page: Int, limit: Int) async throws -> [TaggedItem<Issue>] {
        let state = stateFilter.rawValue
        let query = searchText
        let issueType = config.issueType
        let flags = config.involvementFlags
        let sources = manager.connectionSources()

        let batches = try await withThrowingTaskGroup(of: (Int, [Issue]).self) { group in
            for (index, source) in sources.enumerated() {
                let client = source.client
                if query.isEmpty {
                    for flag in flags {
                        group.addTask {
                            let service = IssueService(client: client)
                            return try await (index, resilientFetch {
                                try await service.searchIssues(
                                    type: issueType, state: state, page: page, limit: limit,
                                    assigned: flag == .assigned, created: flag == .created,
                                    mentioned: flag == .mentioned,
                                    reviewRequested: flag == .reviewRequested,
                                )
                            })
                        }
                    }
                } else {
                    group.addTask {
                        let service = IssueService(client: client)
                        return try await (index, resilientFetch {
                            try await service.searchIssues(
                                type: issueType, state: state, query: query,
                                page: page, limit: limit,
                            )
                        })
                    }
                }
            }
            return try await group.reduce(into: [(Int, [Issue])]()) { $0.append($1) }
        }

        return TaggedItem.mergeAndDeduplicate(batches: batches, sources: sources)
            .sorted { $0.item.updatedAt > $1.item.updatedAt }
    }
}
