import UIKit

@MainActor
final class IssuesViewController: RefreshingTableViewController {
    private let context: AppContext
    private let owner: String
    private let repository: String
    private var rows: [IssueRow] = []
    private var filterOptions: IssueFilterOptions?
    private var filterTask: Task<Void, Never>?
    private var currentPage: UInt32 = 0
    private lazy var filterButton = UIBarButtonItem(
        image: context.symbol("line.3.horizontal.decrease.circle")
    )

    init(context: AppContext, owner: String, repository: String) {
        self.context = context
        self.owner = owner
        self.repository = repository
        super.init()
        title = repository
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    deinit { filterTask?.cancel() }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
        let addButton = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(createIssue)
        )
        addButton.accessibilityLabel = "New issue"
        navigationItem.rightBarButtonItems = [addButton, filterButton]
        updateFilterMenu()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        loadFilterOptions()
        loadIssues(refreshing: refreshing)
    }

    private func loadIssues(refreshing: Bool) {
        loadIssues(page: 1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        loadIssues(page: currentPage + 1, refreshing: false)
    }

    private func loadIssues(page: UInt32, refreshing: Bool) {
        if page == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let result = try await context.core.issues(
                    owner: owner,
                    repository: repository,
                    page: page
                )
                if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
                currentPage = page
                finishPagination(hasMore: result.hasMore)
                tableView.reloadData()
                updateEmptyState()
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if page == 1 { endLoading() }
        }
    }

    private func loadFilterOptions() {
        filterTask?.cancel()
        filterTask = Task {
            do {
                let options = try await context.core.issueFilters(
                    owner: owner,
                    repository: repository
                )
                guard !Task.isCancelled else { return }
                filterOptions = options
                updateFilterMenu()
                updateEmptyState()
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        rows.count
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
        cell.configure(rows[indexPath.row])
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        navigationController?.pushViewController(
            IssueViewController(
                context: context,
                owner: owner,
                repository: repository,
                number: rows[indexPath.row].number
            ),
            animated: true
        )
    }

    private func updateFilterMenu() {
        let current = context.core.settings().issueStatus
        let status = UIMenu(
            title: "Status",
            image: context.symbol("circle.lefthalf.filled"),
            options: .singleSelection,
            children: ["open", "closed"].map { status in
                UIAction(
                    title: status.capitalized,
                    state: current == status ? .on : .off
                ) { [weak self] _ in
                    guard let self else { return }
                    do {
                        try self.context.core.setIssueStatus(status: status)
                        self.updateFilterMenu()
                        self.loadIssues(refreshing: false)
                    } catch {
                        self.show(error: error)
                    }
                }
            }
        )
        var children: [UIMenuElement] = [status]
        if let filterOptions {
            children.append(milestoneMenu(filterOptions))
            children.append(labelMenu(filterOptions))
        } else {
            children.append(UIAction(title: "Loading filters…", attributes: .disabled) { _ in })
        }
        filterButton.menu = UIMenu(children: children)
        filterButton.accessibilityLabel = "Filter issues"
        updateFilterTint()
    }

    private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
        let selected = options.selectedMilestone
        let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
            [weak self] _ in self?.selectMilestone("")
        }
        let actions = options.milestones.map { milestone in
                UIAction(
                    title: milestone,
                    state: selected == milestone ? .on : .off
                ) { [weak self] _ in self?.selectMilestone(milestone) }
            }
        return UIMenu(
            title: "Milestone",
            image: context.symbol("flag"),
            options: .singleSelection,
            children: [all] + actions
        )
    }

    private func labelMenu(_ options: IssueFilterOptions) -> UIMenu {
        let selected = Set(options.selectedLabels)
        let actions = options.labels.map { label in
            UIAction(
                title: options.unavailableLabels.contains(label) ? "\(label) (Unavailable)" : label,
                attributes: .keepsMenuPresented,
                state: selected.contains(label) ? .on : .off
            ) { [weak self] action in
                guard let self, let options = self.filterOptions else { return }
                self.filterTask?.cancel()
                var labels = Set(options.selectedLabels)
                if labels.remove(label) == nil { labels.insert(label) }
                do {
                    try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
                    self.filterOptions?.selectedLabels = Array(labels)
                    action.state = labels.contains(label) ? .on : .off
                    self.updateFilterMenu()
                    self.loadIssues(refreshing: false)
                } catch {
                    self.show(error: error)
                }
            }
        }
        return UIMenu(
            title: "Labels",
            image: context.symbol("tag"),
            children: actions.isEmpty
                ? [UIAction(title: "No labels", attributes: .disabled) { _ in }]
                : actions
        )
    }

    private func selectMilestone(_ milestone: String) {
        guard let options = filterOptions else { return }
        filterTask?.cancel()
        do {
            try saveFilters(milestone: milestone, labels: Set(options.selectedLabels))
            filterOptions?.selectedMilestone = milestone
            updateFilterMenu()
            loadIssues(refreshing: false)
        } catch {
            show(error: error)
        }
    }

    private func saveFilters(milestone: String, labels: Set<String>) throws {
        try context.core.setIssueFilters(
            owner: owner,
            repository: repository,
            milestone: milestone,
            labels: Array(labels)
        )
    }

    private var filtersActive: Bool {
        (try? context.core.issueFiltersActive(owner: owner, repository: repository)) ?? false
    }

    private func updateFilterTint() {
        filterButton.tintColor = filtersActive ? .tintColor : .secondaryLabel
        filterButton.accessibilityValue = filtersActive
            ? "Filters active"
            : "Default filters"
    }

    @objc private func createIssue() {
        let editor = IssueEditorViewController(
            context: context,
            owner: owner,
            repository: repository
        ) { [weak self] number in
            guard let self else { return }
            self.dismiss(animated: true) {
                self.loadContent(refreshing: false)
                self.navigationController?.pushViewController(
                    IssueViewController(
                        context: self.context,
                        owner: self.owner,
                        repository: self.repository,
                        number: number
                    ),
                    animated: true
                )
            }
        }
        present(UINavigationController(rootViewController: editor), animated: true)
    }

    private func updateEmptyState() {
        guard rows.isEmpty else {
            tableView.backgroundView = nil
            return
        }
        let status = context.core.settings().issueStatus
        tableView.backgroundView = EmptyBackgroundView(
            title: "No \(status) issues",
            detail: filtersActive
                ? "No issues match the selected filters."
                : "This repository has no \(status) issues."
        )
    }
}

final class IssueCell: UITableViewCell {
    private let stateIcon = UIImageView()
    private let titleLabel = UILabel()
    private let summaryLabel = UILabel()
    private let labels = UIStackView()
    private let metaLabel = UILabel()
    private let milestoneLabel = UILabel()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        accessoryType = .disclosureIndicator
        titleLabel.font = .preferredFont(forTextStyle: .headline)
        titleLabel.numberOfLines = 2
        let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
        titleStack.alignment = .firstBaseline
        titleStack.spacing = 8
        summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
        summaryLabel.textColor = .secondaryLabel
        summaryLabel.numberOfLines = 2
        labels.axis = .horizontal
        labels.spacing = 5
        metaLabel.font = .preferredFont(forTextStyle: .caption1)
        metaLabel.textColor = .tertiaryLabel
        metaLabel.numberOfLines = 2
        milestoneLabel.font = .preferredFont(forTextStyle: .caption1)
        milestoneLabel.adjustsFontForContentSizeCategory = true
        let stack = UIStackView(
            arrangedSubviews: [titleStack, summaryLabel, labels, metaLabel, milestoneLabel]
        )
        stack.axis = .vertical
        stack.spacing = 6
        stack.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
            stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func configure(_ row: IssueRow) {
        configureIssueStateIcon(stateIcon, state: row.state, textStyle: .headline)
        titleLabel.text = row.title
        summaryLabel.text = row.summary
        metaLabel.text = row.meta
        milestoneLabel.isHidden = row.milestone.isEmpty
        milestoneLabel.attributedText = symbolText(
            "flag.fill",
            text: row.milestone,
            font: milestoneLabel.font,
            color: .tertiaryLabel
        )
        milestoneLabel.accessibilityLabel = row.milestone.isEmpty
            ? nil
            : "Milestone \(row.milestone)"
        labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
        labels.isHidden = row.labels.isEmpty
        for label in row.labels.prefix(3) {
            let view = UILabel()
            view.text = "  \(label.name)  "
            view.font = .preferredFont(forTextStyle: .caption2)
            view.textColor = label.light ? .black : .white
            view.backgroundColor = UIColor(hex: label.color)
            view.layer.cornerRadius = 9
            view.clipsToBounds = true
            labels.addArrangedSubview(view)
        }
        labels.addArrangedSubview(UIView())
    }
}

@MainActor
final class MilestonesViewController: RefreshingTableViewController {
    private let context: AppContext
    private let owner: String
    private let repository: String
    private var rows: [MilestoneRow] = []
    private var currentPage: UInt32 = 0

    init(context: AppContext, owner: String, repository: String) {
        self.context = context
        self.owner = owner
        self.repository = repository
        super.init()
        title = repository
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 118
        let addButton = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(createMilestone)
        )
        addButton.accessibilityLabel = "New milestone"
        navigationItem.rightBarButtonItem = addButton
        loadContent(refreshing: false)
    }

    @objc private func createMilestone() {
        let editor = MilestoneEditorViewController(
            context: context,
            owner: owner,
            repository: repository
        ) { [weak self] _ in
            guard let self else { return }
            self.dismiss(animated: true) { self.loadContent(refreshing: false) }
        }
        present(UINavigationController(rootViewController: editor), animated: true)
    }

    override func loadContent(refreshing: Bool) {
        loadPage(1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        loadPage(currentPage + 1, refreshing: false)
    }

    private func loadPage(_ page: UInt32, refreshing: Bool) {
        if page == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let result = try await context.core.milestones(
                    owner: owner,
                    repository: repository,
                    page: page
                )
                if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
                currentPage = page
                finishPagination(hasMore: result.hasMore)
                tableView.reloadData()
                tableView.backgroundView = rows.isEmpty
                    ? EmptyBackgroundView(
                        title: "No milestones",
                        detail: "This repository does not have any milestones."
                    )
                    : nil
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if page == 1 { endLoading() }
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        rows.count
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: "milestone",
            for: indexPath
        ) as! MilestoneCell
        cell.configure(rows[indexPath.row], disclosure: true)
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        navigationController?.pushViewController(
            MilestoneViewController(
                context: context,
                owner: owner,
                repository: repository,
                id: rows[indexPath.row].id
            ),
            animated: true
        )
    }
}

@MainActor
final class MilestoneViewController: RefreshingTableViewController {
    private let context: AppContext
    private let owner: String
    private let repository: String
    private let id: Int64
    private var page: MilestonePage?
    private var currentPage: UInt32 = 0

    init(context: AppContext, owner: String, repository: String, id: Int64) {
        self.context = context
        self.owner = owner
        self.repository = repository
        self.id = id
        super.init()
        title = "Milestone"
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
        tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 118
        let editButton = UIBarButtonItem(
            image: context.symbol("pencil"),
            style: .plain,
            target: self,
            action: #selector(editMilestone)
        )
        editButton.accessibilityLabel = "Edit milestone"
        navigationItem.rightBarButtonItem = editButton
        loadContent(refreshing: false)
    }

    @objc private func editMilestone() {
        let editor = MilestoneEditorViewController(
            context: context,
            owner: owner,
            repository: repository,
            id: id
        ) { [weak self] _ in
            guard let self else { return }
            self.dismiss(animated: true) { self.loadContent(refreshing: false) }
        }
        present(UINavigationController(rootViewController: editor), animated: true)
    }

    override func loadContent(refreshing: Bool) {
        loadPage(1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        loadPage(currentPage + 1, refreshing: false)
    }

    private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
        if requestedPage == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let result = try await context.core.milestone(
                    owner: owner,
                    repository: repository,
                    id: id,
                    page: requestedPage
                )
                if requestedPage == 1 {
                    page = result
                } else {
                    page?.issues.append(contentsOf: result.issues)
                    page?.pulls.append(contentsOf: result.pulls)
                    page?.hasMore = result.hasMore
                }
                currentPage = requestedPage
                finishPagination(hasMore: result.hasMore)
                title = page?.milestone.title
                tableView.reloadData()
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if requestedPage == 1 { endLoading() }
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 3 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0: 1
        case 1: page?.issues.count ?? 0
        default: page?.pulls.count ?? 0
        }
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        switch section {
        case 1: "Issues"
        case 2: "Pull Requests"
        default: nil
        }
    }

    override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        if section == 1, page?.issues.isEmpty == true {
            return "No issues are assigned to this milestone."
        }
        if section == 2, page?.pulls.isEmpty == true {
            return "No pull requests are assigned to this milestone."
        }
        return nil
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        guard let page else { return UITableViewCell() }
        if indexPath.section == 0 {
            let cell = tableView.dequeueReusableCell(
                withIdentifier: "milestone",
                for: indexPath
            ) as! MilestoneCell
            cell.configure(page.milestone, disclosure: false)
            return cell
        }
        if indexPath.section == 1 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
            cell.configure(page.issues[indexPath.row])
            return cell
        }
        let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
            ?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
        let pull = page.pulls[indexPath.row]
        configureTextCell(cell, title: pull.title, detail: "\(pull.summary)\n\(pull.meta)")
        cell.accessoryType = .disclosureIndicator
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        if indexPath.section == 1, let issue = page?.issues[indexPath.row] {
            navigationController?.pushViewController(
                IssueViewController(
                    context: context,
                    owner: owner,
                    repository: repository,
                    number: issue.number
                ),
                animated: true
            )
        } else if indexPath.section == 2, let pull = page?.pulls[indexPath.row] {
            navigationController?.pushViewController(
                PullViewController(
                    context: context,
                    owner: pull.owner,
                    repository: pull.repository,
                    number: pull.number
                ),
                animated: true
            )
        }
    }
}

final class MilestoneCell: UITableViewCell {
    private let titleLabel = UILabel()
    private let descriptionLabel = UILabel()
    private let metaLabel = UILabel()
    private let progress = UIProgressView(progressViewStyle: .bar)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        titleLabel.font = .preferredFont(forTextStyle: .headline)
        titleLabel.numberOfLines = 2
        descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
        descriptionLabel.textColor = .secondaryLabel
        descriptionLabel.numberOfLines = 2
        metaLabel.font = .preferredFont(forTextStyle: .caption1)
        metaLabel.textColor = .tertiaryLabel
        metaLabel.numberOfLines = 2
        progress.progressTintColor = .systemGreen
        let stack = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, progress, metaLabel])
        stack.axis = .vertical
        stack.spacing = 7
        stack.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
            stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func configure(_ row: MilestoneRow, disclosure: Bool) {
        accessoryType = disclosure ? .disclosureIndicator : .none
        titleLabel.text = row.title
        descriptionLabel.text = row.description
        metaLabel.text = row.meta
        progress.progress = Float(row.progress)
        progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
        progress.accessibilityLabel = "Milestone progress"
        progress.accessibilityValue = row.progressAccessibility
    }
}

@MainActor
final class PullsViewController: RefreshingTableViewController {
    private let context: AppContext
    private var rows: [PullRow] = []
    private var filterOptions: PullFilterOptions?
    private var filterTask: Task<Void, Never>?
    private var currentPage: UInt32 = 0

    init(context: AppContext) {
        self.context = context
        super.init()
        title = "Pull Requests"
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    deinit { filterTask?.cancel() }

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            image: context.symbol("server.rack"),
            primaryAction: UIAction { [weak self] _ in
                guard let self else { return }
                self.navigationController?.pushViewController(
                    ServersViewController(context: self.context),
                    animated: true
                )
            }
        )
        updateFilterMenu()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        guard context.core.activeServerIndex() != nil else {
            tableView.backgroundView = EmptyBackgroundView(
                title: "No server selected",
                detail: "Open Issues or Repos to select a server or add your first one."
            )
            refreshControl?.endRefreshing()
            return
        }
        loadFilterOptions()
        loadPulls(refreshing: refreshing)
    }

    private func loadPulls(refreshing: Bool) {
        loadPulls(page: 1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        loadPulls(page: currentPage + 1, refreshing: false)
    }

    private func loadPulls(page: UInt32, refreshing: Bool) {
        if page == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let result = try await context.core.pulls(page: page)
                if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
                currentPage = page
                finishPagination(hasMore: result.hasMore)
                tableView.reloadData()
                let status = context.core.settings().pullStatus
                tableView.backgroundView = rows.isEmpty
                    ? EmptyBackgroundView(
                        title: "No \(status) pull requests",
                        detail: filtersActive
                            ? "No pull requests match the selected filters."
                            : "No pull requests match the selected status."
                    )
                    : nil
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if page == 1 { endLoading() }
        }
    }

    private func loadFilterOptions() {
        filterTask?.cancel()
        filterTask = Task {
            do {
                filterOptions = try await context.core.pullFilters()
                guard !Task.isCancelled else { return }
                updateFilterMenu()
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        rows.count
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
            ?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
        let row = rows[indexPath.row]
        configureTextCell(
            cell,
            title: row.title,
            detail: "\(row.summary)\n\(row.meta)"
        )
        cell.accessoryType = .disclosureIndicator
        return cell
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        116
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let row = rows[indexPath.row]
        navigationController?.pushViewController(
            PullViewController(
                context: context,
                owner: row.owner,
                repository: row.repository,
                number: row.number
            ),
            animated: true
        )
    }

    private func updateFilterMenu() {
        let current = context.core.settings().pullStatus
        let status = UIMenu(
            title: "Status",
            image: context.symbol("circle.lefthalf.filled"),
            options: .singleSelection,
            children: ["open", "closed"].map { status in
                UIAction(title: status.capitalized, state: current == status ? .on : .off) {
                    [weak self] _ in
                    guard let self else { return }
                    do {
                        try self.context.core.setPullStatus(status: status)
                        self.updateFilterMenu()
                        self.loadPulls(refreshing: false)
                    } catch {
                        self.show(error: error)
                    }
                }
            }
        )
        let milestone: UIMenuElement = filterOptions.map(milestoneMenu)
            ?? UIAction(title: "Loading milestones…", attributes: .disabled) { _ in }
        let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem(
            image: context.symbol("line.3.horizontal.decrease.circle")
        )
        item.menu = UIMenu(children: [status, milestone])
        item.accessibilityLabel = "Filter pull requests"
        navigationItem.rightBarButtonItem = item
        updateFilterTint()
    }

    private func milestoneMenu(_ options: PullFilterOptions) -> UIMenu {
        let selected = options.selectedMilestone
        let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
            [weak self] _ in self?.selectMilestone("")
        }
        let milestones = options.milestones.map { milestone in
            UIAction(title: milestone, state: selected == milestone ? .on : .off) {
                [weak self] _ in self?.selectMilestone(milestone)
            }
        }
        return UIMenu(
            title: "Milestone",
            image: context.symbol("flag"),
            options: .singleSelection,
            children: [all] + milestones
        )
    }

    private func selectMilestone(_ milestone: String) {
        filterTask?.cancel()
        do {
            try context.core.setPullFilters(milestone: milestone)
            filterOptions?.selectedMilestone = milestone
            updateFilterMenu()
            loadPulls(refreshing: false)
        } catch {
            show(error: error)
        }
    }

    private var filtersActive: Bool {
        (try? context.core.pullFiltersActive()) ?? false
    }

    private func updateFilterTint() {
        navigationItem.rightBarButtonItem?.tintColor = filtersActive ? .tintColor : .secondaryLabel
        navigationItem.rightBarButtonItem?.accessibilityValue = filtersActive
            ? "Filters active"
            : "Default filters"
    }
}

@MainActor
final class CommitsViewController: RefreshingTableViewController {
    private enum Mode: Int { case history, files }

    private let context: AppContext
    private let owner: String
    private let repository: String
    private var page: CommitPage?
    private var contents: [RepositoryContentRow] = []
    private var branch: String?
    private var mode = Mode.history
    private var currentPage: UInt32 = 0

    init(context: AppContext, owner: String, repository: String) {
        self.context = context
        self.owner = owner
        self.repository = repository
        super.init()
        title = repository
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
        let modeControl = UISegmentedControl(items: ["History", "Files"])
        modeControl.selectedSegmentIndex = mode.rawValue
        modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
        modeControl.accessibilityLabel = "Repository view"
        navigationItem.titleView = modeControl
        updateBranchMenu()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        loadPage(1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        guard mode == .history else { return }
        loadPage(currentPage + 1, refreshing: false)
    }

    private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
        if requestedPage == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                switch mode {
                case .history:
                    page = try await context.core.commits(
                        owner: owner,
                        repository: repository,
                        branch: branch,
                        path: "",
                        pages: requestedPage
                    )
                    currentPage = requestedPage
                    finishPagination(hasMore: page?.hasMore ?? false)
                    updateBranchMenu()
                case .files:
                    contents = try await context.core.repositoryContents(
                        owner: owner,
                        repository: repository,
                        path: ""
                    )
                }
                tableView.reloadData()
                updateEmptyView()
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if requestedPage == 1 { endLoading() }
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        mode == .history ? page?.commits.count ?? 0 : contents.count
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        switch mode {
        case .history:
            let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
            if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
            return cell
        case .files:
            let cell = tableView.dequeueReusableCell(withIdentifier: "repository-content")
                ?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content")
            configureRepositoryContentCell(cell, row: contents[indexPath.row])
            return cell
        }
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        mode == .history ? 86 : UITableView.automaticDimension
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        switch mode {
        case .history:
            guard let row = page?.commits[indexPath.row] else { return }
            navigationController?.pushViewController(
                FilesViewController(
                    context: context,
                    owner: owner,
                    repository: repository,
                    sha: row.sha
                ),
                animated: true
            )
        case .files:
            showRepositoryContent(
                contents[indexPath.row],
                context: context,
                owner: owner,
                repository: repository,
                navigationController: navigationController
            )
        }
    }

    @objc private func modeChanged(_ sender: UISegmentedControl) {
        guard let mode = Mode(rawValue: sender.selectedSegmentIndex), mode != self.mode else { return }
        self.mode = mode
        navigationItem.rightBarButtonItem = nil
        if mode == .history { updateBranchMenu() }
        tableView.reloadData()
        loadContent(refreshing: false)
    }

    private func updateEmptyView() {
        switch mode {
        case .history:
            tableView.backgroundView = page?.commits.isEmpty == true
                ? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
                : nil
        case .files:
            tableView.backgroundView = contents.isEmpty
                ? EmptyBackgroundView(title: "No files", detail: "This repository is empty.")
                : nil
        }
    }

    private func updateBranchMenu() {
        let branches = page?.branches ?? []
        let choices: [String?] = [nil] + branches.map(Optional.some)
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: branch ?? "All",
            menu: UIMenu(children: choices.map { choice in
                UIAction(
                    title: choice ?? "All",
                    state: choice == branch ? .on : .off
                ) { [weak self] _ in
                    self?.branch = choice
                    self?.updateBranchMenu()
                    self?.loadContent(refreshing: false)
                }
            })
        )
    }
}

final class CommitCell: UITableViewCell {
    private static let laneOrigin: CGFloat = 12
    private static let laneSpacing: CGFloat = 12

    private var row: CommitRow?
    private var laneCount: UInt32 = 0

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        accessoryType = .disclosureIndicator
        backgroundColor = .systemBackground
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func configure(_ row: CommitRow, laneCount: UInt32) {
        self.row = row
        self.laneCount = laneCount
        var content = defaultContentConfiguration()
        content.directionalLayoutMargins.leading = laneCount == 0
            ? 0
            : CGFloat(laneCount) * Self.laneSpacing + 10
        content.text = row.title
        content.secondaryText = row.detail
        content.secondaryTextProperties.numberOfLines = 2
        contentConfiguration = content
        setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let row, laneCount > 0, let context = UIGraphicsGetCurrentContext() else { return }
        let centerY = bounds.midY
        let nodeX = row.nodeLane.map(laneX)
        context.setLineWidth(3)
        context.setLineCap(.round)
        context.setLineJoin(.round)

        for lane in 0..<laneCount {
            if row.topLanes.contains(lane) {
                let start = CGPoint(x: laneX(lane), y: 0)
                if row.topConnections.contains(lane), let nodeX {
                    strokeCurve(
                        context,
                        from: start,
                        to: CGPoint(x: nodeX, y: centerY),
                        color: laneColor(lane)
                    )
                } else {
                    strokeLine(
                        context,
                        from: start,
                        to: CGPoint(x: start.x, y: centerY),
                        color: laneColor(lane)
                    )
                }
            }
            if row.bottomLanes.contains(lane) {
                let end = CGPoint(x: laneX(lane), y: bounds.height)
                if !row.bottomConnections.contains(lane) || row.topLanes.contains(lane) {
                    strokeLine(
                        context,
                        from: CGPoint(x: end.x, y: centerY),
                        to: end,
                        color: laneColor(lane)
                    )
                }
                if row.bottomConnections.contains(lane), let nodeX {
                    strokeCurve(
                        context,
                        from: CGPoint(x: nodeX, y: centerY),
                        to: end,
                        color: laneColor(lane)
                    )
                }
            }
        }
        if let lane = row.nodeLane, let nodeX {
            context.setFillColor(laneColor(lane).cgColor)
            context.fillEllipse(
                in: CGRect(x: nodeX - 5, y: centerY - 5, width: 10, height: 10)
            )
        }
    }

    private func laneX(_ lane: UInt32) -> CGFloat {
        Self.laneOrigin + CGFloat(lane) * Self.laneSpacing
    }

    private func strokeLine(
        _ context: CGContext,
        from start: CGPoint,
        to end: CGPoint,
        color: UIColor
    ) {
        context.setStrokeColor(color.cgColor)
        context.move(to: start)
        context.addLine(to: end)
        context.strokePath()
    }

    private func strokeCurve(
        _ context: CGContext,
        from start: CGPoint,
        to end: CGPoint,
        color: UIColor
    ) {
        let middleY = (start.y + end.y) / 2
        context.setStrokeColor(color.cgColor)
        context.move(to: start)
        context.addCurve(
            to: end,
            control1: CGPoint(x: start.x, y: middleY),
            control2: CGPoint(x: end.x, y: middleY)
        )
        context.strokePath()
    }

    private func laneColor(_ lane: UInt32) -> UIColor {
        UIColor(hue: CGFloat((Double(lane) * 137.508).truncatingRemainder(dividingBy: 360)) / 360,
                saturation: 0.78, brightness: 0.78, alpha: 1)
    }
}
