import Foundation

// MARK: - Dev Flag

/// Master flag: flip to `false` for production builds.
/// Controls API endpoint, dev banner, and magic OTPs.
let IS_DEV = true

// MARK: - API Errors

enum APIError: LocalizedError {
    case invalidURL
    case noData
    case decodingError(String)
    case serverError(String)
    case unauthorized
    case networkError(String)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .noData: return "No data received"
        case .decodingError(let msg): return "Decoding error: \(msg)"
        case .serverError(let msg): return msg
        case .unauthorized: return "Unauthorized"
        case .networkError(let msg): return msg
        }
    }
}

// MARK: - Login Response

struct LoginResponse {
    let userId: Int
    let userFirstName: String
    let token: String
    let photoUrl: String
}

// MARK: - Chat Messages Result

struct ChatMessagesResult {
    let messages: [ChatMessage]
    let chatClosed: Bool
}

// MARK: - API Service

actor APIService {
    static let shared = APIService()

    private static let devBaseURL  = "https://dev.payfrit.com/api"
    private static let prodBaseURL = "https://biz.payfrit.com/api"

    private var userToken: String?
    private var userId: Int?
    private var businessId: Int = 0

    var baseURL: String { IS_DEV ? Self.devBaseURL : Self.prodBaseURL }

    // MARK: - Configuration

    func setAuth(token: String?, userId: Int?) {
        self.userToken = token
        self.userId = userId
    }

    func setBusinessId(_ id: Int) {
        self.businessId = id
    }

    func getToken() -> String? { userToken }
    func getUserId() -> Int? { userId }
    func getBusinessId() -> Int { businessId }

    // MARK: - Core Request

    private func postJSON(_ path: String, payload: [String: Any]) async throws -> [String: Any] {
        let urlString = baseURL + (path.hasPrefix("/") ? path : "/\(path)")
        guard let url = URL(string: urlString) else { throw APIError.invalidURL }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")

        if let token = userToken, !token.isEmpty {
            request.setValue(token, forHTTPHeaderField: "X-User-Token")
        }
        if businessId > 0 {
            request.setValue(String(businessId), forHTTPHeaderField: "X-Business-ID")
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: payload)

        let (data, response) = try await URLSession.shared.data(for: request)

        if let httpResponse = response as? HTTPURLResponse {
            if httpResponse.statusCode == 401 { throw APIError.unauthorized }
            guard (200...299).contains(httpResponse.statusCode) else {
                throw APIError.serverError("HTTP \(httpResponse.statusCode)")
            }
        }

        // Try to parse JSON, handling CFML prefix junk
        if let json = tryDecodeJSON(data) {
            return json
        }
        throw APIError.decodingError("Non-JSON response")
    }

    private func tryDecodeJSON(_ data: Data) -> [String: Any]? {
        // Try direct parse
        if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            return json
        }
        // Try extracting JSON from mixed response
        guard let body = String(data: data, encoding: .utf8),
              let start = body.firstIndex(of: "{"),
              let end = body.lastIndex(of: "}") else { return nil }
        let jsonStr = String(body[start...end])
        guard let jsonData = jsonStr.data(using: .utf8) else { return nil }
        return try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
    }

    private func ok(_ json: [String: Any]) -> Bool {
        if let b = json["OK"] as? Bool { return b }
        if let b = json["ok"] as? Bool { return b }
        if let i = json["OK"] as? Int { return i == 1 }
        if let s = json["OK"] as? String { return s == "true" || s == "1" || s == "YES" }
        return false
    }

    private func err(_ json: [String: Any]) -> String {
        (json["ERROR"] as? String) ?? (json["error"] as? String)
            ?? (json["Error"] as? String) ?? (json["message"] as? String) ?? ""
    }

    /// All expected PascalCase keys our models use.
    nonisolated private static let expectedKeys: [String] = [
        "TaskID", "TaskBusinessID", "TaskCategoryID", "TaskTypeID", "TaskTitle",
        "TaskDetails", "TaskCreatedOn", "TaskStatusID", "TaskSourceType", "TaskSourceID",
        "TaskCategoryName", "TaskCategoryColor",
        "EmployeeID", "BusinessID", "BusinessName", "BusinessAddress", "BusinessCity",
        "EmployeeStatusID", "PendingTaskCount",
        "OrderID", "OrderRemarks", "OrderSubmittedOn",
        "ServicePointID", "ServicePointName", "ServicePointTypeID",
        "DeliveryAddress", "DeliveryLat", "DeliveryLng",
        "CustomerUserID", "CustomerFirstName", "CustomerLastName",
        "CustomerPhone", "CustomerPhotoUrl", "BeaconUUID",
        "LineItems", "TableMembers",
        "LineItemID", "ItemName", "Quantity", "PriceCents", "Remark",
        "IsModifier", "ParentLineItemID",
        "UserID", "FirstName", "LastName", "IsHost",
        "MessageID", "SenderUserID", "SenderType", "SenderName", "Text", "MessageText",
        "CreatedOn", "IsRead",
        "OK", "ERROR", "TASKS", "BUSINESSES", "MESSAGES", "TASK",
        "UserFirstName", "Token", "PhotoUrl", "UserPhotoUrl",
        "TIER", "STRIPE", "ACTIVATION",
        "HasAccount", "PayoutsEnabled", "SetupIncomplete",
        "BalanceCents", "CapCents", "RemainingCents", "IsComplete", "ProgressPercent",
        "ENTRIES", "TOTALS", "ID",
        "GrossEarningsCents", "ActivationWithheldCents", "NetTransferCents", "Status", "CreatedAt",
        "TotalGrossCents", "TotalWithheldCents", "TotalNetCents",
        "AccountID", "ACCOUNT_ID", "URL", "url",
        "CHAT_CLOSED", "chat_closed", "PayCents"
    ]

    /// Build a lookup: stripped key (no underscores, lowercased) → expected PascalCase key.
    /// This handles ALL CAPS, underscore_separated, camelCase, PascalCase — any format.
    nonisolated private static let keyLookup: [String: String] = {
        var map: [String: String] = [:]
        for k in expectedKeys {
            // Strip underscores and lowercase for fuzzy matching
            let stripped = k.replacingOccurrences(of: "_", with: "").lowercased()
            map[stripped] = k
            // Also add exact uppercased (for direct ALL CAPS match)
            map[k.uppercased()] = k
        }
        return map
    }()

    /// Normalize dictionary keys: maps ANY casing/format to expected PascalCase.
    /// Strips underscores and lowercases for fuzzy matching against known keys.
    nonisolated static func normalizeKeys(_ dict: [String: Any]) -> [String: Any] {
        var result: [String: Any] = [:]
        for (key, value) in dict {
            // Try exact uppercased match first, then stripped fuzzy match
            let stripped = key.replacingOccurrences(of: "_", with: "").lowercased()
            let normalizedKey = keyLookup[key.uppercased()] ?? keyLookup[stripped] ?? key
            if let arr = value as? [[String: Any]] {
                result[normalizedKey] = arr.map { normalizeKeys($0) }
            } else if let sub = value as? [String: Any] {
                result[normalizedKey] = normalizeKeys(sub)
            } else {
                result[normalizedKey] = value
            }
        }
        return result
    }

    /// Find an array value in the JSON by trying multiple key variants
    nonisolated static func findArray(_ json: [String: Any], _ keys: [String]) -> [[String: Any]]? {
        for key in keys {
            if let arr = json[key] as? [[String: Any]] { return arr }
        }
        // Fallback: search all values for the first array of dicts
        for (_, value) in json {
            if let arr = value as? [[String: Any]], !arr.isEmpty { return arr }
        }
        return nil
    }

    // MARK: - Auth

    func login(username: String, password: String) async throws -> LoginResponse {
        let json = try await postJSON("/auth/login.cfm", payload: [
            "username": username,
            "password": password
        ])

        guard ok(json) else {
            let e = err(json)
            if e == "bad_credentials" {
                throw APIError.serverError("Invalid email/phone or password")
            }
            throw APIError.serverError("Login failed: \(e)")
        }

        let uid = (json["UserID"] as? Int)
            ?? Int(json["UserID"] as? String ?? "")
            ?? (json["UserId"] as? Int)
            ?? 0
        let token = (json["Token"] as? String)
            ?? (json["token"] as? String)
            ?? ""
        let firstName = (json["UserFirstName"] as? String)
            ?? (json["FirstName"] as? String)
            ?? (json["firstName"] as? String)
            ?? (json["Name"] as? String)
            ?? (json["name"] as? String)
            ?? ""
        let photoUrl = (json["UserPhotoUrl"] as? String)
            ?? (json["PhotoUrl"] as? String)
            ?? (json["photoUrl"] as? String)
            ?? ""

        self.userToken = token
        self.userId = uid

        return LoginResponse(userId: uid, userFirstName: firstName, token: token, photoUrl: photoUrl)
    }

    func logout() {
        userToken = nil
        userId = nil
        businessId = 0
    }

    // MARK: - Businesses

    func getMyBusinesses() async throws -> [Employment] {
        guard let uid = userId, uid > 0 else {
            throw APIError.serverError("User not logged in")
        }

        let json = try await postJSON("/workers/myBusinesses.cfm", payload: [
            "UserID": uid
        ])

        guard ok(json) else {
            throw APIError.serverError("Failed to load businesses: \(err(json))")
        }

        guard let arr = Self.findArray(json, ["BUSINESSES", "Businesses", "businesses"]) else {
            return []
        }
        return arr.map { Employment(json: $0) }
    }

    // MARK: - Tasks

    func listPendingTasks(categoryId: Int? = nil) async throws -> [WorkTask] {
        var payload: [String: Any] = ["BusinessID": businessId]
        if let cid = categoryId, cid > 0 {
            payload["CategoryID"] = cid
        }

        let json = try await postJSON("/tasks/listPending.cfm", payload: payload)
        guard ok(json) else {
            throw APIError.serverError("Failed to load tasks: \(err(json))")
        }

        guard let arr = Self.findArray(json, ["TASKS", "Tasks", "tasks"]) else { return [] }
        return arr.map { WorkTask(json: $0) }
    }

    func acceptTask(taskId: Int) async throws {
        // Flutter only sends TaskID + BusinessID (no UserID)
        let json = try await postJSON("/tasks/accept.cfm", payload: [
            "TaskID": taskId,
            "BusinessID": businessId
        ])

        guard ok(json) else {
            let e = err(json)
            if e == "already_accepted" {
                throw APIError.serverError("This task has already been claimed by someone else.")
            }
            throw APIError.serverError("Failed to accept task: \(e)")
        }
    }

    func listMyTasks(filterType: String = "active") async throws -> [WorkTask] {
        var payload: [String: Any] = [
            "UserID": userId ?? 0,
            "FilterType": filterType
        ]
        if businessId > 0 {
            payload["BusinessID"] = businessId
        }

        let json = try await postJSON("/tasks/listMine.cfm", payload: payload)
        guard ok(json) else {
            throw APIError.serverError("Failed to load my tasks: \(err(json))")
        }

        guard let arr = Self.findArray(json, ["TASKS", "Tasks", "tasks"]) else { return [] }
        return arr.map { WorkTask(json: $0) }
    }

    func completeTask(taskId: Int) async throws {
        let json = try await postJSON("/tasks/complete.cfm", payload: [
            "TaskID": taskId,
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to complete task: \(err(json))")
        }
    }

    func closeChat(taskId: Int) async throws {
        let json = try await postJSON("/tasks/completeChat.cfm", payload: [
            "TaskID": taskId
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to close chat: \(err(json))")
        }
    }

    func getTaskDetails(taskId: Int) async throws -> TaskDetails {
        let json = try await postJSON("/tasks/getDetails.cfm", payload: [
            "TaskID": taskId
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to load task details: \(err(json))")
        }

        // Find the TASK object — try known keys, then fallback to first dict value
        var taskJson: [String: Any]?
        for key in ["TASK", "Task", "task"] {
            if let d = json[key] as? [String: Any] { taskJson = d; break }
        }
        if taskJson == nil {
            // Fallback: look for first nested dict that looks like a task
            for (_, value) in json {
                if let d = value as? [String: Any], d.count > 3 { taskJson = d; break }
            }
        }
        guard let taskJson = taskJson else {
            throw APIError.serverError("Invalid task details response")
        }

        let details = TaskDetails(json: taskJson)
        return details
    }

    // MARK: - Chat

    func getChatMessages(taskId: Int, afterMessageId: Int? = nil) async throws -> ChatMessagesResult {
        var payload: [String: Any] = ["TaskID": taskId]
        if let after = afterMessageId, after > 0 {
            payload["AfterMessageID"] = after
        }

        let json = try await postJSON("/chat/getMessages.cfm", payload: payload)
        guard ok(json) else {
            throw APIError.serverError("Failed to load chat messages")
        }

        let arr = Self.findArray(json, ["MESSAGES", "Messages", "messages"]) ?? []
        let messages: [ChatMessage] = arr.map { ChatMessage(json: $0) }
        let chatClosed = (json["CHAT_CLOSED"] as? Bool) == true || (json["chat_closed"] as? Bool) == true

        return ChatMessagesResult(messages: messages, chatClosed: chatClosed)
    }

    func sendChatMessage(taskId: Int, message: String, userId: Int? = nil, senderType: String? = nil) async throws -> Int {
        var payload: [String: Any] = [
            "TaskID": taskId,
            "Message": message
        ]
        if let uid = userId { payload["UserID"] = uid }
        if let st = senderType { payload["SenderType"] = st }

        let json = try await postJSON("/chat/sendMessage.cfm", payload: payload)
        guard ok(json) else {
            throw APIError.serverError("Failed to send message")
        }

        return (json["MessageID"] as? Int) ?? (json["MESSAGE_ID"] as? Int) ?? 0
    }

    func markChatMessagesRead(taskId: Int, readerType: String) async throws {
        let json = try await postJSON("/chat/markRead.cfm", payload: [
            "TaskID": taskId,
            "ReaderType": readerType
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to mark messages as read")
        }
    }

    // MARK: - Payout / Tier Endpoints

    func getTierStatus() async throws -> TierStatus {
        let json = try await postJSON("/workers/tierStatus.cfm", payload: [
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to load tier status: \(err(json))")
        }
        let data = try JSONSerialization.data(withJSONObject: json)
        return (try? JSONDecoder().decode(TierStatus.self, from: data)) ?? TierStatus()
    }

    func createStripeAccount() async throws -> String {
        let json = try await postJSON("/workers/createAccount.cfm", payload: [
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to create Stripe account: \(err(json))")
        }
        return (json["AccountID"] as? String) ?? (json["ACCOUNT_ID"] as? String) ?? ""
    }

    func getOnboardingLink() async throws -> String {
        let json = try await postJSON("/workers/onboardingLink.cfm", payload: [
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to get onboarding link: \(err(json))")
        }
        return (json["URL"] as? String) ?? (json["url"] as? String) ?? ""
    }

    func getEarlyUnlockUrl() async throws -> String {
        let json = try await postJSON("/workers/earlyUnlock.cfm", payload: [
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to get unlock URL: \(err(json))")
        }
        return (json["URL"] as? String) ?? (json["url"] as? String) ?? ""
    }

    func getLedger() async throws -> LedgerResponse {
        let json = try await postJSON("/workers/ledger.cfm", payload: [
            "UserID": userId ?? 0
        ])
        guard ok(json) else {
            throw APIError.serverError("Failed to load ledger: \(err(json))")
        }
        let data = try JSONSerialization.data(withJSONObject: json)
        return try JSONDecoder().decode(LedgerResponse.self, from: data)
    }

    // MARK: - Debug

    /// Returns raw JSON string for a given endpoint (for debugging key issues)
    func debugRawJSON(_ path: String, payload: [String: Any]) async -> String {
        let urlString = baseURL + (path.hasPrefix("/") ? path : "/\(path)")
        guard let url = URL(string: urlString) else { return "Invalid URL" }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        if let token = userToken, !token.isEmpty {
            request.setValue(token, forHTTPHeaderField: "X-User-Token")
        }
        if businessId > 0 {
            request.setValue(String(businessId), forHTTPHeaderField: "X-Business-ID")
        }
        request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
        guard let (data, _) = try? await URLSession.shared.data(for: request) else {
            return "Network error"
        }
        let raw = String(data: data, encoding: .utf8) ?? "Non-UTF8"
        // Return first 2000 chars
        return String(raw.prefix(2000))
    }

    // MARK: - URL Helpers

    /// Resolve a photo URL — if it starts with "/" prepend the base domain
    nonisolated static func resolvePhotoUrl(_ rawUrl: String) -> String {
        let trimmed = rawUrl.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty { return "" }
        if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return trimmed }
        let baseDomain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com"
        if trimmed.hasPrefix("/") { return baseDomain + trimmed }
        return baseDomain + "/" + trimmed
    }

    // MARK: - Date Parsing

    private static let iso8601Formatter: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f
    }()

    private static let iso8601NoFrac: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime]
        return f
    }()

    private static let simpleDateFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd HH:mm:ss"
        f.locale = Locale(identifier: "en_US_POSIX")
        return f
    }()

    private static let cfmlDateFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "MMMM, dd yyyy HH:mm:ss Z"
        f.locale = Locale(identifier: "en_US_POSIX")
        return f
    }()

    private static let cfmlShortFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        f.locale = Locale(identifier: "en_US_POSIX")
        return f
    }()

    private static let cfmlAltFormatters: [DateFormatter] = {
        let formats = [
            "MMM dd, yyyy HH:mm:ss",
            "MM/dd/yyyy HH:mm:ss",
            "yyyy-MM-dd HH:mm:ss.S",
            "yyyy-MM-dd'T'HH:mm:ss.S",
            "yyyy-MM-dd'T'HH:mm:ssZ",
            "yyyy-MM-dd'T'HH:mm:ss.SZ",
        ]
        return formats.map { fmt in
            let f = DateFormatter()
            f.dateFormat = fmt
            f.locale = Locale(identifier: "en_US_POSIX")
            return f
        }
    }()

    nonisolated static func parseDate(_ string: String) -> Date? {
        let s = string.trimmingCharacters(in: .whitespacesAndNewlines)
        if s.isEmpty { return nil }

        // Try epoch (milliseconds or seconds)
        if let epoch = Double(s) {
            if epoch > 1_000_000_000_000 { return Date(timeIntervalSince1970: epoch / 1000) }
            if epoch > 1_000_000_000 { return Date(timeIntervalSince1970: epoch) }
        }

        if let d = iso8601Formatter.date(from: s) { return d }
        if let d = iso8601NoFrac.date(from: s) { return d }
        if let d = simpleDateFormatter.date(from: s) { return d }
        if let d = cfmlDateFormatter.date(from: s) { return d }
        if let d = cfmlShortFormatter.date(from: s) { return d }
        for formatter in cfmlAltFormatters {
            if let d = formatter.date(from: s) { return d }
        }
        return nil
    }

}


