//
//  CrashReporter.swift
//  NetNewsWire
//
//  Created by Brent Simmons on 12/17/18.
//  Copyright © 2018 Ranchero Software. All rights reserved.
//

import Foundation
import RSWeb

// Based originally on Uli Kusterer's UKCrashReporter: http://www.zathras.de/angelweb/blog-ukcrashreporter-oh-one.htm
// Then based on the crash reporter included in NetNewsWire 3 and NetNewsWire Lite 4.
// Displays a window that shows the crash log — gives the user the chance to add data.
// (Or just decide not to send it.)
// This code is not included in the MAS build.
// At some point this code should probably move into RSCore, so Rainier and any other
// future apps can use it.


struct CrashLog {

	let path: String
	let modificationDate: Date
	let content: String
	let contentHash: String

	init?(path: String, modificationDate: Date) {
		guard let s = try? NSString(contentsOfFile: path, usedEncoding: nil) as String, !s.isEmpty else {
			return nil
		}
		self.content = s
		self.contentHash = s.rs_md5Hash()
		self.path = path
		self.modificationDate = modificationDate
	}
}

struct CrashReporter {

	struct DefaultsKey {
		static let lastSeenCrashLogDateKey = "LastSeenCrashLogDate"
		static let hashOfLastSeenCrashLogKey = "LastSeenCrashLogHash"
		static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
	}

	private static var crashReportWindowController: CrashReportWindowController?

	/// Look in ~/Library/Logs/DiagnosticReports/ for a new crash log for this app.
	/// Show a crash log reporter window if found.
	static func check(appName: String) {
		let folder = ("~/Library/Logs/DiagnosticReports/" as NSString).expandingTildeInPath
		guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: folder) else {
			return
		}

		let crashSuffix = ".crash"
		let lowerAppName = appName.lowercased()
		let lastSeenCrashLogDate: Date = {
			let lastSeenCrashLogDouble = UserDefaults.standard.double(forKey: DefaultsKey.lastSeenCrashLogDateKey)
			return Date(timeIntervalSince1970: lastSeenCrashLogDouble)
		}()

		var mostRecentFilePath: String? = nil
		var mostRecentFileDate = Date.distantPast
		for filename in filenames {
			if !filename.lowercased().hasPrefix(lowerAppName) || !filename.hasSuffix(crashSuffix) {
				continue
			}

			let path = (folder as NSString).appendingPathComponent(filename)
			let fileAttributes = (try? FileManager.default.attributesOfItem(atPath: path)) ?? [FileAttributeKey: Any]()
			if let fileModificationDate = fileAttributes[.modificationDate] as? Date {
				if fileModificationDate > lastSeenCrashLogDate && fileModificationDate > mostRecentFileDate { // Ignore if previously seen
					mostRecentFileDate = fileModificationDate
					mostRecentFilePath = path
				}
			}
		}

		guard let crashLogPath = mostRecentFilePath, let crashLog = CrashLog(path: crashLogPath, modificationDate: mostRecentFileDate) else {
			return
		}

		if hasSeen(crashLog) {
			return
		}
		remember(crashLog)

		if shouldSendCrashLogsAutomatically() {
			sendCrashLogText(crashLog.content)
		}
		else {
			runCrashReporterWindow(crashLog)
		}
	}

	static func sendCrashLogText(_ crashLogText: String) {
		var request = URLRequest(url: URL(string: "https://ranchero.com/netnewswire/crashreportcatcher.php")!)
		request.httpMethod = HTTPMethod.post

		let boundary = "0xKhTmLbOuNdArY"

		let contentType = "multipart/form-data; boundary=\(boundary)"
		request.setValue(contentType, forHTTPHeaderField:HTTPRequestHeader.contentType)

		let formString = "--\(boundary)\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n\(crashLogText)\r\n--\(boundary)--\r\n"
		let formData = formString.data(using: .utf8, allowLossyConversion: true)
		request.httpBody = formData

		download(request) { (_, _, _) in
			// Don’t care about the result.
		}
	}

	static func runCrashReporterWindow(_ crashLog: CrashLog) {
		crashReportWindowController = CrashReportWindowController(crashLogText: crashLog.content)
		crashReportWindowController!.showWindow(self)
	}
}

private extension CrashReporter {

	static func hasSeen(_ crashLog: CrashLog) -> Bool {
		// No need to compare dates, because that’s done in the file loop.
		// Check to see if we’ve already reported this exact crash log.
		guard let hashOfLastSeenCrashLog = UserDefaults.standard.string(forKey: DefaultsKey.hashOfLastSeenCrashLogKey) else {
			return false
		}
		return hashOfLastSeenCrashLog == crashLog.contentHash
	}

	static func remember(_ crashLog: CrashLog) {
		// Save the modification date and hash, so we don’t send duplicates.
		UserDefaults.standard.set(crashLog.contentHash, forKey: DefaultsKey.hashOfLastSeenCrashLogKey)
		UserDefaults.standard.set(crashLog.modificationDate.timeIntervalSince1970, forKey: DefaultsKey.lastSeenCrashLogDateKey)
	}

	static func shouldSendCrashLogsAutomatically() -> Bool {
		return UserDefaults.standard.bool(forKey: DefaultsKey.sendCrashLogsAutomaticallyKey)
	}
}