import Foundation
import SQLite3
import Swifter
import Contacts
import Photos
import EventKit
import ScriptingBridge

/// HTTP server running on localhost:8765. Serves two surfaces:
///
/// 1. **Settings API** — `GET /api/settings` and `PUT /api/settings` let the
///    web dashboard read and write `~/.config/com.lilith.mac-sync/config.json`.
///
/// 2. **Static SPA** — everything else is served from the bundled `webapp/`
///    directory with SPA fallback to `index.html` for client-side routing.
///
/// The server finds the webapp root by checking:
/// - `Bundle.main.resourcePath/webapp/` (installed .app bundle)
/// - `<binary-dir>/../../web/dist/` (dev: `make run` or `swift run`)
public final class LocalWebServer {
    private let server: HttpServer
    public let port: UInt16

    public init() {
        let configured = UserDefaults.standard.integer(forKey: "webServerPort")
        port = UInt16(configured > 0 ? configured : 8765)
        server = HttpServer()
    }

    public func start() {
        registerRoutes()
        do {
            try server.start(port, forceIPv4: true)
            NSLog("LocalWebServer: listening on http://localhost:\(port)")
        } catch {
            NSLog("LocalWebServer: start failed — \(error)")
        }
    }

    public func stop() {
        server.stop()
    }

    // MARK: - Routes

    private func registerRoutes() {
        server.GET["/api/settings"] = { [weak self] _ in
            guard let self else { return .internalServerError }
            return jsonResponse(readSettings())
        }
        server.PUT["/api/settings"] = { [weak self] req in
            guard let self else { return .internalServerError }
            let bodyData = Data(req.body)
            guard !bodyData.isEmpty,
                  let raw = try? JSONSerialization.jsonObject(with: bodyData),
                  let updates = raw as? [String: Any] else {
                return .badRequest(.text("invalid JSON"))
            }
            applySettings(updates)
            return jsonResponse(readSettings())
        }
        server.GET["/api/diagnostics"] = { [weak self] _ in
            guard let self else { return .internalServerError }
            return jsonResponse(diagnosticsPayload())
        }
        server.GET["/api/activity"] = { _ in
            let logPath = FileManager.default.homeDirectoryForCurrentUser
                .appendingPathComponent("Library/Application Support/MacSync/stderr.log").path
            guard let content = try? String(contentsOfFile: logPath, encoding: .utf8) else {
                return jsonArrayResponse([])
            }
            let lines = content.components(separatedBy: "\n")
            let activityPrefix = "] ActivityLog: ["
            let entries: [[String: String]] = lines
                .filter { $0.contains(activityPrefix) }
                .suffix(100)
                .reversed()
                .compactMap { line -> [String: String]? in
                    guard let prefixRange = line.range(of: activityPrefix) else { return nil }
                    let rest = String(line[prefixRange.upperBound...])
                    let level: String
                    let message: String
                    if rest.hasPrefix("info] ") {
                        level = "info"; message = String(rest.dropFirst(6))
                    } else if rest.hasPrefix("success] ") {
                        level = "success"; message = String(rest.dropFirst(9))
                    } else if rest.hasPrefix("error] ") {
                        level = "error"; message = String(rest.dropFirst(7))
                    } else if rest.hasPrefix("warning] ") {
                        level = "warning"; message = String(rest.dropFirst(9))
                    } else {
                        level = "info"; message = rest
                    }
                    // Extract ISO-ish timestamp from line prefix "2026-04-23 19:53:37.578 ..."
                    let parts = line.components(separatedBy: " ")
                    let ts = parts.count >= 2 ? "\(parts[0])T\(parts[1])Z" : ""
                    return ["timestamp": ts, "level": level, "message": message]
                }
            return jsonArrayResponse(entries)
        }

        server.GET["/api/imessage/service-for-handle"] = { req in
            guard let rawHandle = req.queryParams.first(where: { $0.0 == "handle" })?.1,
                  !rawHandle.isEmpty else {
                return jsonResponse(["error": "missing_handle"])
            }
            let handle = rawHandle.removingPercentEncoding ?? rawHandle
            let chatDbPath = (NSHomeDirectory() as NSString).appendingPathComponent("Library/Messages/chat.db")
            var db: OpaquePointer?
            guard sqlite3_open_v2(chatDbPath, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX, nil) == SQLITE_OK else {
                return jsonResponse(["error": "chat_db_unavailable"])
            }
            defer { sqlite3_close(db) }
            let sql = "SELECT service FROM message WHERE handle_id IN (SELECT ROWID FROM handle WHERE id=?) AND is_from_me=0 ORDER BY date DESC LIMIT 1"
            var stmt: OpaquePointer?
            guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
                return jsonResponse(["error": "prepare_failed"])
            }
            defer { sqlite3_finalize(stmt) }
            sqlite3_bind_text(stmt, 1, (handle as NSString).utf8String, -1, unsafeBitCast(-1, to: sqlite3_destructor_type.self))
            var rawSvc: String? = nil
            if sqlite3_step(stmt) == SQLITE_ROW, let col = sqlite3_column_text(stmt, 0) {
                rawSvc = String(cString: col)
            }
            let service: String
            if rawSvc == "iMessage" || rawSvc == "SMS" {
                service = rawSvc!
            } else {
                service = "unknown"
            }
            var result: [String: Any] = ["service": service]
            if let raw = rawSvc { result["raw_service"] = raw }
            return jsonResponse(result)
        }

        // Static files + SPA fallback
        server.notFoundHandler = { [weak self] req in
            guard let webRoot = self?.webappDirectory() else { return .notFound }
            if req.path.hasPrefix("/api/") { return .notFound }

            let filePath = (webRoot as NSString).appendingPathComponent(req.path)
            if let data = FileManager.default.contents(atPath: filePath),
               !filePath.hasSuffix("/") {
                return .ok(.data(data, contentType: mimeType(for: filePath)))
            }

            let index = (webRoot as NSString).appendingPathComponent("index.html")
            guard let indexData = FileManager.default.contents(atPath: index) else {
                return .notFound
            }
            return .ok(.data(indexData, contentType: "text/html"))
        }
    }

    // MARK: - Settings

    private func readSettings() -> [String: Any] {
        let ud = UserDefaults.standard
        var result: [String: Any] = [:]
        result["serverURL"] = ud.string(forKey: "serverURL") ?? "http://10.0.0.11:3201"
        let p = ud.integer(forKey: "webServerPort")
        result["webServerPort"] = p > 0 ? p : 8765
        if let token = ud.string(forKey: "serviceToken") { result["serviceToken"] = token }
        if let name = ud.string(forKey: "deviceName") { result["deviceName"] = name }
        if let addrs = ud.array(forKey: "imessageOwnAddresses") { result["imessageOwnAddresses"] = addrs }
        if let addrs = ud.array(forKey: "imailOwnAddresses") { result["imailOwnAddresses"] = addrs }
        return result
    }

    private func applySettings(_ updates: [String: Any]) {
        let ud = UserDefaults.standard
        for (key, value) in updates {
            ud.set(value, forKey: key)
        }
        ConfigFile.merge(updates)
        NSLog("LocalWebServer: settings updated — \(updates.keys.sorted().joined(separator: ", "))")
    }

    private func diagnosticsPayload() -> [String: Any] {
        let chatDbPath = FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Library/Messages/chat.db").path
        let fullDiskAccess = Darwin.access(chatDbPath, R_OK) == 0

        var iMessageDb: [String: Any] = [:]
        if fullDiskAccess && FileManager.default.fileExists(atPath: chatDbPath) {
            iMessageDb["available"] = true
            iMessageDb["error"] = NSNull()
        } else {
            iMessageDb["available"] = false
            if !fullDiskAccess {
                iMessageDb["error"] = "Full Disk Access not granted"
            } else {
                iMessageDb["error"] = "chat.db not found"
            }
        }

        var mailScriptingBridge: [String: Any] = [:]
        if SBApplication(bundleIdentifier: "com.apple.mail") != nil {
            mailScriptingBridge["available"] = true
            mailScriptingBridge["error"] = NSNull()
        } else {
            mailScriptingBridge["available"] = false
            mailScriptingBridge["error"] = "Mail.app not accessible via ScriptingBridge"
        }

        let calendarAuth = authStatusString(for: EKEventStore.authorizationStatus(for: .event))
        let photosAuth = photosAuthStatusString(for: PHPhotoLibrary.authorizationStatus(for: .readWrite))
        let contactsAuth = contactsAuthStatusString(for: CNContactStore.authorizationStatus(for: .contacts))

        return [
            "fullDiskAccess": fullDiskAccess,
            "iMessageDb": iMessageDb,
            "mailScriptingBridge": mailScriptingBridge,
            "calendarAuth": calendarAuth,
            "photosAuth": photosAuth,
            "contactsAuth": contactsAuth
        ]
    }

    private func authStatusString(for status: EKAuthorizationStatus) -> String {
        if #available(macOS 14.0, *) {
            switch status {
            case .fullAccess:      return "authorized"
            case .writeOnly:       return "authorized"
            case .denied:          return "denied"
            case .notDetermined:   return "notDetermined"
            case .restricted:      return "restricted"
            @unknown default:      return "notDetermined"
            }
        } else {
            switch status {
            case .authorized:      return "authorized"
            case .denied:          return "denied"
            case .notDetermined:   return "notDetermined"
            case .restricted:      return "restricted"
            @unknown default:      return "notDetermined"
            }
        }
    }

    private func photosAuthStatusString(for status: PHAuthorizationStatus) -> String {
        switch status {
        case .authorized:      return "authorized"
        case .limited:         return "limited"
        case .denied:          return "denied"
        case .notDetermined:   return "notDetermined"
        case .restricted:      return "restricted"
        @unknown default:      return "notDetermined"
        }
    }

    private func contactsAuthStatusString(for status: CNAuthorizationStatus) -> String {
        switch status {
        case .authorized:      return "authorized"
        case .denied:          return "denied"
        case .notDetermined:   return "notDetermined"
        case .restricted:      return "restricted"
        @unknown default:      return "notDetermined"
        }
    }

    // MARK: - Webapp root discovery

    private func webappDirectory() -> String? {
        if let res = Bundle.main.resourcePath {
            let candidate = (res as NSString).appendingPathComponent("webapp")
            if FileManager.default.fileExists(atPath: candidate) { return candidate }
        }
        // Dev path: binary is at .build/debug/MacSyncApp
        // web/dist is at <repo-root>/web/dist
        let binaryPath = ProcessInfo.processInfo.arguments[0]
        let buildDebug = (binaryPath as NSString).deletingLastPathComponent          // .build/debug
        let buildDir = (buildDebug as NSString).deletingLastPathComponent             // .build
        let repoRoot = (buildDir as NSString).deletingLastPathComponent               // repo root
        let devDist = (repoRoot as NSString).appendingPathComponent("web/dist")
        let resolved = (devDist as NSString).standardizingPath
        if FileManager.default.fileExists(atPath: resolved) { return resolved }
        NSLog("LocalWebServer: webapp directory not found")
        return nil
    }
}

// MARK: - HTTP helpers

private func jsonArrayResponse(_ arr: [[String: String]]) -> HttpResponse {
    guard let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
          let str = String(data: data, encoding: .utf8) else {
        return .internalServerError
    }
    return .raw(200, "OK",
        ["Content-Type": "application/json", "Access-Control-Allow-Origin": "*"]
    ) { writer in try writer.write([UInt8](str.utf8)) }
}

private func jsonResponse(_ dict: [String: Any]) -> HttpResponse {
    guard let data = try? JSONSerialization.data(withJSONObject: dict, options: [.sortedKeys]),
          let str = String(data: data, encoding: .utf8) else {
        return .internalServerError
    }
    return .raw(200, "OK",
        ["Content-Type": "application/json", "Access-Control-Allow-Origin": "*"]
    ) { writer in try writer.write([UInt8](str.utf8)) }
}

private func mimeType(for path: String) -> String {
    switch (path as NSString).pathExtension.lowercased() {
    case "html":        return "text/html; charset=utf-8"
    case "js", "mjs":  return "application/javascript"
    case "css":         return "text/css"
    case "json":        return "application/json"
    case "png":         return "image/png"
    case "jpg", "jpeg": return "image/jpeg"
    case "svg":         return "image/svg+xml"
    case "ico":         return "image/x-icon"
    case "woff":        return "font/woff"
    case "woff2":       return "font/woff2"
    default:            return "application/octet-stream"
    }
}
