import UIKit

@MainActor
class MarkdownPageViewController: UIViewController {
    let context: AppContext
    let scrollView = UIScrollView()
    let stack = UIStackView()
    private let spinner = UIActivityIndicatorView(style: .medium)
    var loadingTask: Task<Void, Never>?

    init(context: AppContext) {
        self.context = context
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGroupedBackground
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.alwaysBounceVertical = true
        scrollView.refreshControl = UIRefreshControl()
        scrollView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
        view.addSubview(scrollView)
        stack.axis = .vertical
        stack.spacing = 12
        stack.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stack)
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 18),
            stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -18),
            stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 18),
            stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -18),
            stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -36),
        ])
    }

    deinit { loadingTask?.cancel() }

    func loadContent(refreshing: Bool) {}

    func beginLoading(refreshing: Bool) {
        if !refreshing { beginNavigationLoading(spinner) }
    }

    func endLoading() {
        endNavigationLoading(spinner)
        scrollView.refreshControl?.endRefreshing()
    }

    func replaceContent(_ views: [UIView]) {
        stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        views.forEach(stack.addArrangedSubview)
    }

    @objc private func refreshRequested() {
        loadContent(refreshing: true)
    }
}

@MainActor
final class IssueViewController: MarkdownPageViewController {
    private let owner: String
    private let repository: String
    private let number: Int64

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

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

    override func viewDidLoad() {
        super.viewDidLoad()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        beginLoading(refreshing: refreshing)
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let page = try await context.core.issue(
                    owner: owner,
                    repository: repository,
                    number: number
                )
                replaceContent(detailViews(
                    title: page.title,
                    meta: page.meta,
                    body: page.body,
                    comments: page.comments,
                    milestone: page.milestone
                ))
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
            endLoading()
        }
    }
}

@MainActor
final class PullViewController: MarkdownPageViewController {
    private let owner: String
    private let repository: String
    private let number: Int64

    init(context: AppContext, owner: String, repository: String, number: Int64) {
        self.owner = owner
        self.repository = repository
        self.number = number
        super.init(context: context)
        title = "Pull Request"
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        beginLoading(refreshing: refreshing)
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let page = try await context.core.pull(
                    owner: owner,
                    repository: repository,
                    number: number
                )
                var views = detailViews(
                    title: page.title,
                    meta: page.meta,
                    body: page.body,
                    comments: []
                )
                if !page.files.isEmpty {
                    views.append(sectionLabel(page.filesRef))
                    views.append(contentsOf: page.files.map { file in
                        detailButton(title: file.path, detail: file.status) { [weak self] in
                            guard let self else { return }
                            self.navigationController?.pushViewController(
                                DiffViewController(
                                    context: self.context,
                                    source: .pull(
                                        owner: self.owner,
                                        repository: self.repository,
                                        number: self.number,
                                        path: file.path
                                    )
                                ),
                                animated: true
                            )
                        }
                    })
                }
                views.append(contentsOf: commentViews(page.comments))
                replaceContent(views)
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
            endLoading()
        }
    }
}

@MainActor
final class FilesViewController: RefreshingTableViewController {
    private let context: AppContext
    private let owner: String
    private let repository: String
    private let sha: String
    private var rows: [FileRow] = []

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

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

    override func viewDidLoad() {
        super.viewDidLoad()
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        beginLoading(refreshing: refreshing)
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                rows = try await context.core.commitFiles(
                    owner: owner,
                    repository: repository,
                    sha: sha
                )
                tableView.reloadData()
                tableView.backgroundView = rows.isEmpty
                    ? EmptyBackgroundView(title: "No changed files", detail: "This commit does not contain file changes.")
                    : nil
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
            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: "file")
            ?? UITableViewCell(style: .subtitle, reuseIdentifier: "file")
        let row = rows[indexPath.row]
        configureTextCell(cell, title: row.path, detail: row.status)
        cell.accessoryType = .disclosureIndicator
        return cell
    }

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

enum DiffSource {
    case commit(owner: String, repository: String, sha: String, path: String)
    case pull(owner: String, repository: String, number: Int64, path: String)
}

@MainActor
final class DiffViewController: UIViewController {
    private let context: AppContext
    private let source: DiffSource
    private let textView = UITextView()
    private let spinner = UIActivityIndicatorView(style: .medium)
    private var loadingTask: Task<Void, Never>?

    init(context: AppContext, source: DiffSource) {
        self.context = context
        self.source = source
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.isEditable = false
        textView.isSelectable = true
        textView.alwaysBounceVertical = true
        textView.alwaysBounceHorizontal = true
        textView.showsHorizontalScrollIndicator = true
        textView.textContainer.widthTracksTextView = false
        textView.textContainer.lineFragmentPadding = 8
        textView.refreshControl = UIRefreshControl()
        textView.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
        view.addSubview(textView)
        NSLayoutConstraint.activate([
            textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            textView.topAnchor.constraint(equalTo: view.topAnchor),
            textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        beginNavigationLoading(spinner)
        reload()
    }

    deinit { loadingTask?.cancel() }

    @objc private func reload() {
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let page: DiffPage
                switch source {
                case let .commit(owner, repository, sha, path):
                    page = try await context.core.commitDiff(
                        owner: owner,
                        repository: repository,
                        sha: sha,
                        path: path
                    )
                case let .pull(owner, repository, number, path):
                    page = try await context.core.pullDiff(
                        owner: owner,
                        repository: repository,
                        number: number,
                        path: path
                    )
                }
                title = page.title
                textView.attributedText = diffText(page)
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
            endNavigationLoading(spinner)
            textView.refreshControl?.endRefreshing()
        }
    }

    private func diffText(_ page: DiffPage) -> NSAttributedString {
        let output = NSMutableAttributedString()
        let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
        for line in page.lines {
            let text = String(format: "%4@ %4@  %@\n", line.oldNumber, line.newNumber, line.text)
            let color: UIColor
            switch line.kind {
            case "addition": color = UIColor.systemGreen.withAlphaComponent(0.16)
            case "removal": color = UIColor.systemRed.withAlphaComponent(0.16)
            case "hunk": color = UIColor.systemBlue.withAlphaComponent(0.14)
            case "header": color = UIColor.systemGray.withAlphaComponent(0.14)
            default: color = .clear
            }
            output.append(NSAttributedString(string: text, attributes: [
                .font: font,
                .foregroundColor: UIColor.label,
                .backgroundColor: color,
            ]))
        }
        return output
    }
}

private func detailViews(
    title: String,
    meta: String,
    body: String,
    comments: [CommentRow],
    milestone: String = ""
) -> [UIView] {
    let titleLabel = UILabel()
    titleLabel.text = title
    titleLabel.font = .preferredFont(forTextStyle: .title1)
    titleLabel.numberOfLines = 0
    let metaLabel = UILabel()
    metaLabel.text = meta
    metaLabel.font = .preferredFont(forTextStyle: .subheadline)
    metaLabel.textColor = .secondaryLabel
    metaLabel.numberOfLines = 0
    let bodyView = markdownView(body)
    var views: [UIView] = [titleLabel, metaLabel]
    if !milestone.isEmpty {
        let milestoneLabel = UILabel()
        milestoneLabel.attributedText = symbolText(
            "flag.fill",
            text: milestone,
            font: .preferredFont(forTextStyle: .subheadline),
            color: .secondaryLabel
        )
        milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
        views.append(milestoneLabel)
    }
    return views + [separator(), bodyView] + commentViews(comments)
}

private func commentViews(_ comments: [CommentRow]) -> [UIView] {
    guard !comments.isEmpty else { return [] }
    var views: [UIView] = [sectionLabel("Comments")]
    for comment in comments {
        let author = UILabel()
        author.text = comment.author
        author.font = .preferredFont(forTextStyle: .headline)
        let date = UILabel()
        date.text = comment.meta
        date.font = .preferredFont(forTextStyle: .caption1)
        date.textColor = .tertiaryLabel
        let stack = UIStackView(arrangedSubviews: [author, markdownView(comment.body), date, separator()])
        stack.axis = .vertical
        stack.spacing = 6
        views.append(stack)
    }
    return views
}

private func markdownView(_ source: String) -> UITextView {
    let view = UITextView()
    view.attributedText = markdown(source)
    view.isEditable = false
    view.isSelectable = true
    view.isScrollEnabled = false
    view.backgroundColor = .clear
    view.textContainerInset = .zero
    view.textContainer.lineFragmentPadding = 0
    view.adjustsFontForContentSizeCategory = true
    return view
}

private func sectionLabel(_ text: String) -> UILabel {
    let label = UILabel()
    label.text = text
    label.font = .preferredFont(forTextStyle: .headline)
    return label
}

private func separator() -> UIView {
    let line = UIView()
    line.backgroundColor = .separator
    line.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
    return line
}

private func detailButton(title: String, detail: String, action: @escaping () -> Void) -> UIButton {
    var configuration = UIButton.Configuration.plain()
    configuration.title = title
    configuration.subtitle = detail
    configuration.image = UIImage(systemName: "chevron.right")
    configuration.imagePlacement = .trailing
    configuration.imagePadding = 8
    configuration.contentInsets = .init(top: 10, leading: 0, bottom: 10, trailing: 0)
    let button = UIButton(configuration: configuration, primaryAction: UIAction { _ in action() })
    button.contentHorizontalAlignment = .fill
    return button
}
