//
//  ToastConfiguration.swift
//  ToastConfiguration
//
//  Created by Shadowfacts on 8/14/21.
//  Copyright © 2021 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import Sentry
import OSLog
@_spi(InstanceType) import InstanceFeatures

struct ToastConfiguration {
    var systemImageName: String?
    var titleFont: UIFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .boldSystemFont(ofSize: 14))
    var title: String
    var subtitle: String?
    var actionTitle: String?
    var action: ((ToastView) -> Void)?
    var longPressAction: ((ToastView) -> Void)?
    var edgeSpacing: CGFloat = 8
    var edge: Edge = .automatic
    var dismissOnScroll = true
    var dismissAutomaticallyAfter: TimeInterval? = nil
    var onAppear: ((UIViewPropertyAnimator?) -> Void)?
    var onDismiss: ((UIViewPropertyAnimator?) -> Void)?
    
    init(title: String) {
        self.title = title
    }
    
    enum Edge: Equatable {
        case top
        case bottom
        /// Determines edge based on the current device. Bottom on iPhone, top on iPad/Mac.
        case automatic
    }
}

extension ToastConfiguration {
    init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: ((ToastView) -> Void)?) {
        self.init(title: title)
        // localizedDescription is statically dispatched, so we need to call it after the downcast
        if let error = error as? Pachyderm.Client.Error {
            self.subtitle = error.localizedDescription
            self.systemImageName = error.systemImageName
            self.longPressAction = { [unowned viewController] toast in
                toast.dismissToast(animated: true)
                let text = """
                \(title):
                \(error.requestMethod.name) \(error.requestEndpoint)
                
                \(error.type)
                """
                let reporter = IssueReporterViewController.create(reportText: text, dismiss: { [unowned viewController] in
                    viewController.dismiss(animated: true)
                })
                viewController.present(reporter, animated: true)
            }
            // TODO: this is a bizarre place to do this, but code path covers basically all errors
            captureError(error, in: viewController.apiController, title: title)
        } else {
            self.subtitle = error.localizedDescription
            self.systemImageName = "exclamationmark.triangle"
        }
        if let retryAction = retryAction {
            self.actionTitle = "Retry"
            self.action = retryAction
        }
    }
    
    init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @escaping @MainActor (ToastView) async -> Void) {
        self.init(from: error, with: title, in: viewController) { toast in
            Task {
                await retryAction(toast)
            }
        }
    }
}

fileprivate extension Pachyderm.Client.Error {
    var systemImageName: String {
        switch type {
        case .networkError(_):
            return "wifi.exclamationmark"
        case .unexpectedStatus(429):
            return "clock.badge.exclamationmark"
        default:
            return "exclamationmark.triangle"
        }
    }
}

private let toastErrorLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ToastError")

private func captureError(_ error: Client.Error, in mastodonController: MastodonController, title: String) {
    let event = Event(error: error)
    event.message = SentryMessage(formatted: "\(title): \(error)")
    event.tags = [
        "request_method": error.requestMethod.name,
        "request_endpoint": error.requestEndpoint.description,
    ]
    switch error.type {
    case .invalidRequest:
        event.tags!["error_type"] = "invalid_request"
    case .invalidResponse:
        event.tags!["error_type"] = "invalid_response"
    case .invalidModel(let error):
        event.tags!["error_type"] = "invalid_model"
        event.extra = [
            "underlying_error": String(describing: error)
        ]
    case .mastodonError(let code, let error):
        event.tags!["error_type"] = "mastodon_error"
        event.tags!["response_code"] = "\(code)"
        event.extra = [
            "underlying_error": String(describing: error)
        ]
    case .unexpectedStatus(let code):
        event.tags!["error_type"] = "unexpected_status"
        event.tags!["response_code"] = "\(code)"
    default:
        return
    }
    if let code = event.tags!["response_code"],
       code == "401" || code == "403" || code == "404" || code == "422" || code == "500" || code == "502" || code == "503" {
        return
    }
    switch mastodonController.instanceFeatures.instanceType {
    case .mastodon(let mastodonType, let mastodonVersion):
        event.tags!["instance_type"] = "mastodon"
        event.tags!["mastodon_version"] = mastodonVersion?.description ?? "unknown"
        switch mastodonType {
        case .vanilla:
            break
        case .hometown(_):
            event.tags!["mastodon_type"] = "hometown"
        case .glitch:
            event.tags!["mastodon_type"] = "glitch"
        }
    case .pleroma(let pleromaType):
        event.tags!["instance_type"] = "pleroma"
        switch pleromaType {
        case .vanilla(let version):
            event.tags!["pleroma_version"] = version?.description ?? "unknown"
        case .akkoma(let version):
            event.tags!["pleroma_type"] = "akkoma"
            event.tags!["pleroma_version"] = version?.description ?? "unknown"
        }
    case .pixelfed:
        event.tags!["instance_type"] = "pixelfed"
    case .gotosocial:
        event.tags!["instance_type"] = "gotosocial"
    case .firefish(let calckeyVersion):
        event.tags!["instance_type"] = "firefish"
        if let calckeyVersion {
            event.tags!["calckey_version"] = calckeyVersion
        }
    }
    SentrySDK.capture(event: event)
    
    toastErrorLogger.error("\(title, privacy: .public): \(error), \(event.tags!.debugDescription, privacy: .public)")
}
