import Foundation
import Security

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
}

struct APIEnvelope<T: Decodable>: Decodable {
    let code: Int
    let message: String
    let data: T?
}

struct EmptyAPIData: Decodable { }

enum APIError: LocalizedError {
    case invalidConfiguration
    case invalidResponse
    case secureConnection
    case unauthorized
    case server(code: Int, message: String)
    case transport(String)
    case decoding(String)
    case missingData

    var errorDescription: String? {
        switch self {
        case .invalidConfiguration: return "服务地址配置无效"
        case .invalidResponse: return "服务器返回了无效响应"
        case .secureConnection: return "无法安全连接登录服务器，请检查服务器 HTTPS 证书配置"
        case .unauthorized: return "登录状态已失效，请重新登录"
        case .server(_, let message): return message
        case .transport(let message): return "网络连接失败：\(message)"
        case .decoding(let message): return "服务器数据解析失败：\(message)"
        case .missingData: return "服务器响应缺少必要数据"
        }
    }
}

struct AuthTokens: Codable, Sendable {
    let accessToken: String
    let refreshToken: String
    let expiresAt: Date
}

final class TokenStore: @unchecked Sendable {
    static let shared = TokenStore()

    private let service = "com.celestia.trace.authentication"
    private let account = "current-session"

    func load() -> AuthTokens? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
              let data = result as? Data else { return nil }
        return try? JSONDecoder().decode(AuthTokens.self, from: data)
    }

    func save(_ tokens: AuthTokens) throws {
        let data = try JSONEncoder().encode(tokens)
        let baseQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account
        ]
        let attributes: [String: Any] = [
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]
        let status = SecItemUpdate(baseQuery as CFDictionary, attributes as CFDictionary)
        if status == errSecItemNotFound {
            var insert = baseQuery
            attributes.forEach { insert[$0.key] = $0.value }
            guard SecItemAdd(insert as CFDictionary, nil) == errSecSuccess else {
                throw APIError.transport("无法安全保存登录凭据")
            }
        } else if status != errSecSuccess {
            throw APIError.transport("无法更新登录凭据")
        }
    }

    func clear() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account
        ]
        SecItemDelete(query as CFDictionary)
    }
}

private struct RefreshPayload: Decodable {
    let token: String
    let refreshToken: String
    let expiresAt: Date
}

actor APIClient {
    static let shared = APIClient()

    private let baseURL: URL
    private let session: URLSession
    private let tokenStore: TokenStore

    init(
        baseURL: URL? = nil,
        session: URLSession? = nil,
        tokenStore: TokenStore = .shared
    ) {
        let configured = baseURL ?? (Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as? String).flatMap(URL.init(string:))
        self.baseURL = configured ?? URL(string: "https://api.ccdw.life/celestia-trace/v1")!
        if let session {
            self.session = session
        } else {
            let configuration = URLSessionConfiguration.default
            // Authentication is user-initiated. Fail promptly when the device is
            // offline or TLS negotiation fails instead of leaving the UI waiting.
            configuration.waitsForConnectivity = false
            configuration.timeoutIntervalForRequest = 15
            configuration.timeoutIntervalForResource = 60
            self.session = URLSession(configuration: configuration)
        }
        self.tokenStore = tokenStore
    }

    func hasStoredSession() -> Bool {
        tokenStore.load() != nil
    }

    func storeTokens(accessToken: String, refreshToken: String, expiresAt: Date) throws {
        try tokenStore.save(AuthTokens(accessToken: accessToken, refreshToken: refreshToken, expiresAt: expiresAt))
    }

    func clearSession() {
        tokenStore.clear()
    }

    func request<T: Decodable>(
        _ path: String,
        method: HTTPMethod = .get,
        body: Data? = nil,
        authenticated: Bool = false
    ) async throws -> T {
        let envelope: APIEnvelope<T> = try await execute(
            path,
            method: method,
            body: body,
            authenticated: authenticated,
            canRefresh: true
        )
        guard let data = envelope.data else { throw APIError.missingData }
        return data
    }

    func requestVoid(
        _ path: String,
        method: HTTPMethod,
        body: Data? = nil,
        authenticated: Bool = false
    ) async throws {
        let _: APIEnvelope<EmptyAPIData> = try await execute(
            path,
            method: method,
            body: body,
            authenticated: authenticated,
            canRefresh: true
        )
    }

    func upload<T: Decodable>(
        _ path: String,
        fileURL: URL,
        fileName: String,
        mimeType: String,
        fields: [String: String]
    ) async throws -> T {
        do {
            return try await performUpload(path, fileURL: fileURL, fileName: fileName, mimeType: mimeType, fields: fields)
        } catch APIError.unauthorized {
            try await refreshTokens()
            return try await performUpload(path, fileURL: fileURL, fileName: fileName, mimeType: mimeType, fields: fields)
        }
    }

    func download(_ path: String, to destinationURL: URL) async throws {
        do {
            try await performDownload(path, to: destinationURL)
        } catch APIError.unauthorized {
            try await refreshTokens()
            try await performDownload(path, to: destinationURL)
        }
    }

    private func execute<T: Decodable>(
        _ path: String,
        method: HTTPMethod,
        body: Data?,
        authenticated: Bool,
        canRefresh: Bool
    ) async throws -> APIEnvelope<T> {
        var request = try makeRequest(path: path, method: method, body: body)
        if authenticated {
            guard let accessToken = tokenStore.load()?.accessToken else { throw APIError.unauthorized }
            request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        }
        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.data(for: request)
        } catch let error as URLError where Self.isSecureConnectionError(error.code) {
            throw APIError.secureConnection
        } catch {
            throw APIError.transport(error.localizedDescription)
        }
        guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
        if http.statusCode == 401, authenticated, canRefresh {
            try await refreshTokens()
            return try await execute(path, method: method, body: body, authenticated: true, canRefresh: false)
        }
        let envelope: APIEnvelope<T>
        do {
            envelope = try Self.decoder.decode(APIEnvelope<T>.self, from: data)
        } catch {
            throw APIError.decoding(error.localizedDescription)
        }
        guard (200..<300).contains(http.statusCode), envelope.code == 0 else {
            if http.statusCode == 401 { throw APIError.unauthorized }
            throw APIError.server(code: envelope.code, message: envelope.message)
        }
        return envelope
    }

    private func refreshTokens() async throws {
        guard let current = tokenStore.load() else { throw APIError.unauthorized }
        let body = try JSONSerialization.data(withJSONObject: ["refreshToken": current.refreshToken])
        let envelope: APIEnvelope<RefreshPayload> = try await execute(
            "auth/refresh",
            method: .post,
            body: body,
            authenticated: false,
            canRefresh: false
        )
        guard let payload = envelope.data else { throw APIError.unauthorized }
        try tokenStore.save(AuthTokens(accessToken: payload.token, refreshToken: payload.refreshToken, expiresAt: payload.expiresAt))
    }

    private func performUpload<T: Decodable>(
        _ path: String,
        fileURL: URL,
        fileName: String,
        mimeType: String,
        fields: [String: String]
    ) async throws -> T {
        guard let token = tokenStore.load()?.accessToken else { throw APIError.unauthorized }
        let boundary = "CelestiaBoundary-\(UUID().uuidString)"
        let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".upload")
        guard FileManager.default.createFile(atPath: temporaryURL.path, contents: nil),
              let output = try? FileHandle(forWritingTo: temporaryURL) else {
            throw APIError.transport("无法准备上传文件")
        }
        defer {
            try? output.close()
            try? FileManager.default.removeItem(at: temporaryURL)
        }
        for (key, value) in fields.sorted(by: { $0.key < $1.key }) {
            try output.write(contentsOf: Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(key)\"\r\n\r\n\(value)\r\n".utf8))
        }
        try output.write(contentsOf: Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\nContent-Type: \(mimeType)\r\n\r\n".utf8))
        let input = try FileHandle(forReadingFrom: fileURL)
        while let chunk = try input.read(upToCount: 1024 * 1024), !chunk.isEmpty {
            try output.write(contentsOf: chunk)
        }
        try input.close()
        try output.write(contentsOf: Data("\r\n--\(boundary)--\r\n".utf8))
        try output.synchronize()

        var request = try makeRequest(path: path, method: .post, body: nil)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.upload(for: request, fromFile: temporaryURL)
        } catch {
            throw APIError.transport(error.localizedDescription)
        }
        guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
        if http.statusCode == 401 { throw APIError.unauthorized }
        let envelope: APIEnvelope<T>
        do {
            envelope = try Self.decoder.decode(APIEnvelope<T>.self, from: data)
        } catch {
            throw APIError.decoding(error.localizedDescription)
        }
        guard (200..<300).contains(http.statusCode), envelope.code == 0 else {
            throw APIError.server(code: envelope.code, message: envelope.message)
        }
        guard let result = envelope.data else { throw APIError.missingData }
        return result
    }

    private func performDownload(_ path: String, to destinationURL: URL) async throws {
        guard let token = tokenStore.load()?.accessToken else { throw APIError.unauthorized }
        var request = try makeRequest(path: path, method: .get, body: nil)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

        let temporaryURL: URL
        let response: URLResponse
        do {
            (temporaryURL, response) = try await session.download(for: request)
        } catch {
            throw APIError.transport(error.localizedDescription)
        }
        guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
        if http.statusCode == 401 { throw APIError.unauthorized }
        guard (200..<300).contains(http.statusCode) else {
            throw APIError.server(code: http.statusCode, message: "云端文件下载失败")
        }

        let fileManager = FileManager.default
        try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
        if fileManager.fileExists(atPath: destinationURL.path) {
            return
        }
        do {
            try fileManager.moveItem(at: temporaryURL, to: destinationURL)
        } catch {
            throw APIError.transport("无法保存云端文件：\(error.localizedDescription)")
        }
    }

    private func makeRequest(path: String, method: HTTPMethod, body: Data?) throws -> URLRequest {
        let pathAndQuery = path.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false)
        let cleanPath = String(pathAndQuery[0]).trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        let pathURL = cleanPath.split(separator: "/").reduce(baseURL) { partial, component in
            partial.appendingPathComponent(String(component))
        }
        var components = URLComponents(url: pathURL, resolvingAgainstBaseURL: false)
        if pathAndQuery.count == 2 {
            components?.percentEncodedQuery = String(pathAndQuery[1])
        }
        guard let url = components?.url else { throw APIError.invalidConfiguration }
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        if let body {
            request.httpBody = body
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        return request
    }

    static let decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom { decoder in
            let container = try decoder.singleValueContainer()
            let value = try container.decode(String.self)
            let formatter = ISO8601DateFormatter()
            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
            if let date = formatter.date(from: value) { return date }
            formatter.formatOptions = [.withInternetDateTime]
            if let date = formatter.date(from: value) { return date }
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid ISO-8601 date")
        }
        return decoder
    }()

    private static func isSecureConnectionError(_ code: URLError.Code) -> Bool {
        switch code {
        case .secureConnectionFailed,
             .serverCertificateHasBadDate,
             .serverCertificateUntrusted,
             .serverCertificateHasUnknownRoot,
             .serverCertificateNotYetValid,
             .clientCertificateRejected,
             .clientCertificateRequired,
             .appTransportSecurityRequiresSecureConnection:
            return true
        default:
            return false
        }
    }
}
