import ForgejoKit
import SwiftUI

// swiftlint:disable:next type_body_length
struct SearchableOverviewView<Row: View, Detail: View, CreateView: View>: View {
    @State private var authService: AuthenticationService
    @State private var pagination = PaginationState<Issue>()
    @State private var stateFilter: IssueFilterState = .open
    @State private var searchText = ""
    @State private var searchTask: Task<Void, Never>?
    @State private var showCreateFlow = false
    @State private var involvementScope: InvolvementScope = .involved

    private let issueService: IssueService?
    private let issueType: String
    private let navigationTitle: String
    private let searchPrompt: String
    private let emptyTitle: String
    private let emptyOpenIcon: String
    private let emptyClosedIcon: String
    private let itemNoun: String
    private let createButtonId: String
    private let showReviewRequested: Bool
    private let rowContent: (Issue) -> Row
    private let detailContent: (Repository, Int, AuthenticationService) -> Detail
    private let createContent: (Repository, AuthenticationService, Bool, @escaping () -> Void) -> CreateView

    init(
        authService: AuthenticationService,
        issueType: String,
        navigationTitle: String,
        searchPrompt: String,
        emptyTitle: String,
        emptyOpenIcon: String,
        emptyClosedIcon: String,
        itemNoun: String,
        createButtonId: String,
        showReviewRequested: Bool,
        @ViewBuilder row: @escaping (Issue) -> Row,
        @ViewBuilder detail: @escaping (Repository, Int, AuthenticationService) -> Detail,
        @ViewBuilder createView: @escaping (
            Repository, AuthenticationService, Bool, @escaping () -> Void,
        ) -> CreateView,
    ) {
        self.authService = authService
        self.issueType = issueType
        self.navigationTitle = navigationTitle
        self.searchPrompt = searchPrompt
        self.emptyTitle = emptyTitle
        self.emptyOpenIcon = emptyOpenIcon
        self.emptyClosedIcon = emptyClosedIcon
        self.itemNoun = itemNoun
        self.createButtonId = createButtonId
        self.showReviewRequested = showReviewRequested
        rowContent = row
        detailContent = detail
        createContent = createView
        issueService = authService.client.map { IssueService(client: $0) }

        let state = PaginationState<Issue>()
        state.cacheName = issueType
        state.dedupeKey = { $0.id }
        state.loadFromCache()
        _pagination = State(initialValue: state)
    }

    private var emptyDescription: String {
        if !searchText.isEmpty {
            return "No \(itemNoun) matching your search."
        }
        if stateFilter == .open {
            return "All clear, no open \(itemNoun) to review."
        }
        let prefix = stateFilter == .all ? "" : stateFilter.rawValue + " "
        return "No \(prefix)\(itemNoun) found."
    }

    private var hasNonDefaultFilters: Bool {
        stateFilter != .open || involvementScope != .involved
    }

    private var filterSummaryText: String {
        let stateLabel = switch stateFilter {
        case .open: "Open"
        case .closed: "Closed"
        case .all: "All"
        }
        // Scope is ignored during search, only show state
        if !searchText.isEmpty || (stateFilter == .open && involvementScope == .involved) {
            return stateLabel
        }
        let scopeLabel = scopeDisplayLabel(involvementScope)
        return "\(stateLabel) · \(scopeLabel)"
    }

    private func scopeDisplayLabel(_ scope: InvolvementScope) -> String {
        switch scope {
        case .involved: "All involvement"
        case .created: "Created by you"
        case .assigned: "Assigned to you"
        case .mentioned: "Mentioned"
        case .reviewRequested: "Review requested"
        }
    }

    var body: some View {
        @Bindable var pagination = pagination
        ZStack(alignment: .bottomTrailing) {
            VStack(spacing: 0) {
                List {
                    if pagination.isLoading, pagination.items.isEmpty {
                        LoadingListSection()
                    } else if pagination.items.isEmpty {
                        ContentUnavailableView {
                            Label(emptyTitle, systemImage: stateFilter == .open ? emptyOpenIcon : emptyClosedIcon)
                                .foregroundStyle(stateFilter == .open ? .green : .secondary)
                        } description: {
                            Text(emptyDescription)
                        }
                    } else {
                        Section {
                            ForEach(pagination.items) { issue in
                                if let repository = issue.repository {
                                    NavigationLink {
                                        detailContent(repository, issue.number, authService)
                                    } label: {
                                        rowContent(issue)
                                    }
                                } else {
                                    rowContent(issue)
                                }
                            }

                            if pagination.hasMore {
                                ProgressView()
                                    .frame(maxWidth: .infinity)
                                    .listRowBackground(Color.clear)
                                    .accessibilityIdentifier("load-more-indicator")
                                    .task {
                                        await loadMore()
                                    }
                            }
                        }
                    }
                }
                .listStyle(.insetGrouped)
                .safeAreaInset(edge: .top) {
                    if hasNonDefaultFilters {
                        Text(filterSummaryText)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            .frame(maxWidth: .infinity)
                            .padding(.horizontal)
                            .padding(.vertical, 6)
                            .accessibilityIdentifier("filter-summary")
                    }
                }
            }

            FloatingCreateButton(action: { showCreateFlow = true })
                .accessibilityIdentifier(createButtonId)
        }
        .navigationTitle(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
                        }
                    }
                    Section("Scope") {
                        filterButton("All", isSelected: involvementScope == .involved) {
                            involvementScope = .involved
                        }
                        filterButton("Created by you", isSelected: involvementScope == .created) {
                            involvementScope = .created
                        }
                        filterButton("Assigned to you", isSelected: involvementScope == .assigned) {
                            involvementScope = .assigned
                        }
                        filterButton("Mentioned", isSelected: involvementScope == .mentioned) {
                            involvementScope = .mentioned
                        }
                        if showReviewRequested {
                            filterButton(
                                "Review requested",
                                isSelected: involvementScope == .reviewRequested,
                            ) {
                                involvementScope = .reviewRequested
                            }
                        }
                    }
                } label: {
                    Image(
                        systemName: hasNonDefaultFilters
                            ? "line.3.horizontal.decrease.circle.fill"
                            : "line.3.horizontal.decrease.circle",
                    )
                }
                .accessibilityIdentifier("filter-menu-button")
            }
        }
        .searchable(text: $searchText, prompt: searchPrompt)
        .refreshable {
            await reloadItems().value
        }
        .task {
            if !pagination.hasLoaded {
                reloadItems()
            }
        }
        .onReceive(
            NotificationCenter.default.publisher(
                for: issueType == "pulls" ? .pullRequestsDidChange : .issuesDidChange,
            ),
        ) { _ in
            pagination.invalidate()
            reloadItems()
        }
        .onChange(of: stateFilter) {
            reloadItems(clearItems: true)
        }
        .onChange(of: involvementScope) {
            reloadItems(clearItems: true)
        }
        .debouncedSearch(text: $searchText, task: $searchTask) {
            await reloadItems().value
        }
        .sheet(isPresented: $showCreateFlow) {
            RepositoryPickerView(authService: authService) { repo in
                createContent(repo, authService, true) {
                    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)
            }
        }
    }

    private func searchIssues(service: IssueService, page: Int, limit: Int) async throws -> [Issue] {
        // Forgejo's search (q=) does not work with involvement scope flags
        // (assigned, created, mentioned). When a search query is active, search
        // globally by type and state only. The scoped view returns when the
        // user clears the search.
        if !searchText.isEmpty {
            return try await service.searchIssues(
                type: issueType, state: stateFilter.rawValue, query: searchText,
                page: page, limit: limit,
            )
        }

        switch involvementScope {
        case .involved:
            return try await searchInvolved(service: service, page: page, limit: limit)
        case .created:
            return try await service.searchIssues(
                type: issueType, state: stateFilter.rawValue,
                page: page, limit: limit, created: true,
            )
        case .assigned:
            return try await service.searchIssues(
                type: issueType, state: stateFilter.rawValue,
                page: page, limit: limit, assigned: true,
            )
        case .mentioned:
            return try await service.searchIssues(
                type: issueType, state: stateFilter.rawValue,
                page: page, limit: limit, mentioned: true,
            )
        case .reviewRequested:
            return try await service.searchIssues(
                type: issueType, state: stateFilter.rawValue,
                page: page, limit: limit, reviewRequested: true,
            )
        }
    }

    /// The API uses AND logic when multiple flags are set, so we fire separate
    /// requests per flag to get OR-union semantics. Timeouts return empty.
    private func searchInvolved(service: IssueService, page: Int, limit: Int) async throws -> [Issue] {
        let type = issueType
        let state = stateFilter.rawValue

        let batches = try await withThrowingTaskGroup(of: [Issue].self) { group in
            for flag in involvedFlags {
                group.addTask {
                    try await resilientFetch {
                        try await service.searchIssues(
                            type: type, state: state, page: page, limit: limit,
                            assigned: flag == .assigned, created: flag == .created,
                            mentioned: flag == .mentioned, reviewRequested: flag == .reviewRequested,
                        )
                    }
                }
            }
            return try await group.reduce(into: [[Issue]]()) { $0.append($1) }
        }

        return Self.deduplicateAndSort(batches)
    }

    private var involvedFlags: [InvolvementScope] {
        var flags: [InvolvementScope] = [.created, .assigned, .mentioned]
        if showReviewRequested { flags.append(.reviewRequested) }
        return flags
    }

    private func resilientFetch(_ fetch: () async throws -> [Issue]) async throws -> [Issue] {
        do {
            return try await fetch()
        } catch let urlError as URLError where urlError.code == .timedOut {
            return []
        }
    }

    static func deduplicateAndSort(_ batches: [[Issue]]) -> [Issue] {
        var seen = Set<Int>()
        var merged = [Issue]()
        for issue in batches.flatMap(\.self) where seen.insert(issue.id).inserted {
            merged.append(issue)
        }
        return merged.sorted { $0.updatedAt > $1.updatedAt }
    }

    @discardableResult
    private func reloadItems(clearItems: Bool = false) -> Task<Void, Never> {
        guard let issueService else { return Task {} }
        return pagination.reloadAndCache(clearItems: clearItems) { [self] page, limit in
            try await searchIssues(service: issueService, page: page, limit: limit)
        }
    }

    private func loadMore() async {
        guard let issueService else { return }
        await pagination.loadMoreAndCache { [self] page, limit in
            try await searchIssues(service: issueService, page: page, limit: limit)
        }
    }

    #if DEBUG
        init(
            preview _: Void,
            authService: AuthenticationService,
            items: [Issue],
            @ViewBuilder row: @escaping (Issue) -> Row,
            @ViewBuilder detail: @escaping (Repository, Int, AuthenticationService) -> Detail,
            @ViewBuilder createView: @escaping (
                Repository, AuthenticationService, Bool, @escaping () -> Void,
            ) -> CreateView,
        ) {
            self.authService = authService
            issueService = nil
            issueType = "issues"
            navigationTitle = ""
            searchPrompt = ""
            emptyTitle = ""
            emptyOpenIcon = ""
            emptyClosedIcon = ""
            itemNoun = ""
            createButtonId = ""
            showReviewRequested = false
            rowContent = row
            detailContent = detail
            createContent = createView
            _pagination = State(initialValue: PaginationState(items: items))
        }
    #endif
}
