import Foundation

public typealias UploadProgressHandler = @Sendable (Double) -> Void

private final class UploadProgressTaskDelegate: NSObject, URLSessionTaskDelegate {
    private let onProgress: UploadProgressHandler

    init(onProgress: @escaping UploadProgressHandler) {
        self.onProgress = onProgress
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64
    ) {
        guard totalBytesExpectedToSend > 0 else { return }
        let progress = min(max(Double(totalBytesSent) / Double(totalBytesExpectedToSend), 0), 1)
        onProgress(progress)
    }
}

public actor APIClient {
    public let baseURL: URL
    public var apiToken: String?
    
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder
    
    public init(baseURL: URL, apiToken: String? = nil) {
        self.baseURL = baseURL
        self.apiToken = apiToken
        if self.apiToken == nil && Self.isLocalhost(baseURL) {
            self.apiToken = "dev"
        }
        self.session = URLSession(configuration: .default)
        
        self.decoder = JSONDecoder()
        self.decoder.dateDecodingStrategy = .iso8601
        
        self.encoder = JSONEncoder()
        self.encoder.dateEncodingStrategy = .iso8601
    }
    
    private static func isLocalhost(_ url: URL) -> Bool {
        guard let host = url.host else { return false }
        return host == "localhost" || host == "127.0.0.1"
    }
    
    public func setAPIToken(_ token: String?) {
        self.apiToken = token
    }
    
    private func request(for path: String) -> URLRequest {
        var request = URLRequest(url: baseURL.appendingPathComponent(path))
        if let token = apiToken {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        return request
    }
    
    private func decodeResponse<T: Decodable>(data: Data, response: URLResponse) throws -> T {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError(error: "invalid response")
        }
        
        if httpResponse.statusCode >= 400 {
            if let error = try? decoder.decode(APIError.self, from: data) {
                throw error
            }
            throw APIError(error: "HTTP \(httpResponse.statusCode)")
        }
        
        if T.self == EmptyResponse.self {
            return EmptyResponse() as! T
        }
        
        return try decoder.decode(T.self, from: data)
    }

    private func perform<T: Decodable>(_ request: URLRequest) async throws -> T {
        let (data, response) = try await session.data(for: request)
        return try decodeResponse(data: data, response: response)
    }

    private func performUpload<T: Decodable>(
        _ request: URLRequest,
        body: Data,
        onProgress: UploadProgressHandler?
    ) async throws -> T {
        onProgress?(0)
        let delegate = onProgress.map { UploadProgressTaskDelegate(onProgress: $0) }
        let (data, response) = try await session.upload(for: request, from: body, delegate: delegate)
        onProgress?(1)
        return try decodeResponse(data: data, response: response)
    }
    
    // MARK: - Uploads
    
    public func uploadFile(
        url: URL,
        folder: String? = nil,
        duplicateAction: UploadDuplicateAction? = nil,
        onProgress: UploadProgressHandler? = nil
    ) async throws -> UploadResponse {
        guard let token = apiToken else {
            throw APIError(error: "no API token configured")
        }
        
        let boundary = UUID().uuidString
        var request = URLRequest(url: baseURL.appendingPathComponent("api/uploads"))
        request.httpMethod = "POST"
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        let data = try Data(contentsOf: url)
        let filename = url.lastPathComponent
        
        var body = Data()
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        if let folder, !folder.isEmpty {
            body.append("Content-Disposition: form-data; name=\"folder\"\r\n\r\n".data(using: .utf8)!)
            body.append(folder.data(using: .utf8)!)
            body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
        }
        if let duplicateAction {
            body.append("Content-Disposition: form-data; name=\"duplicateAction\"\r\n\r\n".data(using: .utf8)!)
            body.append(duplicateAction.rawValue.data(using: .utf8)!)
            body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
        }
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
        body.append(data)
        body.append("\r\n".data(using: .utf8)!)
        body.append("--\(boundary)--\r\n".data(using: .utf8)!)
        
        return try await performUpload(request, body: body, onProgress: onProgress)
    }
    
    public func uploadData(
        _ data: Data,
        filename: String,
        folder: String? = nil,
        duplicateAction: UploadDuplicateAction? = nil,
        onProgress: UploadProgressHandler? = nil
    ) async throws -> UploadResponse {
        guard let token = apiToken else {
            throw APIError(error: "no API token configured")
        }
        
        let boundary = UUID().uuidString
        var request = URLRequest(url: baseURL.appendingPathComponent("api/uploads"))
        request.httpMethod = "POST"
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        var body = Data()
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        if let folder, !folder.isEmpty {
            body.append("Content-Disposition: form-data; name=\"folder\"\r\n\r\n".data(using: .utf8)!)
            body.append(folder.data(using: .utf8)!)
            body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
        }
        if let duplicateAction {
            body.append("Content-Disposition: form-data; name=\"duplicateAction\"\r\n\r\n".data(using: .utf8)!)
            body.append(duplicateAction.rawValue.data(using: .utf8)!)
            body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
        }
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
        body.append(data)
        body.append("\r\n".data(using: .utf8)!)
        body.append("--\(boundary)--\r\n".data(using: .utf8)!)
        
        return try await performUpload(request, body: body, onProgress: onProgress)
    }
    
    // MARK: - Attachments
    
    public func listAttachments() async throws -> [AttachmentRecord] {
        let request = self.request(for: "api/attachments")
        let response: AttachmentsListResponse = try await perform(request)
        return response.attachments
    }
    
    public func attachmentURL(hash: String) -> URL {
        baseURL.appendingPathComponent("attachments/\(hash)")
    }
    
    // MARK: - Documents
    
    public func listDocuments() async throws -> [DocumentRecord] {
        let request = self.request(for: "api/documents")
        let response: DocumentsListResponse = try await perform(request)
        return response.documents
    }
    
    public func saveDocument(path: String, content: String, baseHash: String? = nil) async throws -> DocumentSaveResponse {
        var request = self.request(for: "api/documents/\(path)")
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = DocumentSaveRequest(content: content, baseHash: baseHash)
        request.httpBody = try encoder.encode(body)
        
        return try await perform(request)
    }

    public func searchDocuments(query: String) async throws -> [SearchResult] {
        var components = URLComponents(url: baseURL.appendingPathComponent("api/search"), resolvingAgainstBaseURL: false)
        components?.queryItems = [URLQueryItem(name: "q", value: query)]

        guard let url = components?.url else {
            throw APIError(error: "invalid search URL")
        }

        var request = URLRequest(url: url)
        if let token = apiToken {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        request.setValue("application/json", forHTTPHeaderField: "Accept")

        let response: SearchResponse = try await perform(request)
        return response.results
    }
    
    // MARK: - Sync
    
    public func syncInit(deviceId: String, rootPath: String) async throws -> SyncInitResponse {
        var request = self.request(for: "api/sync/init")
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = SyncInitRequest(deviceId: deviceId, rootPath: rootPath)
        request.httpBody = try encoder.encode(body)
        
        return try await perform(request)
    }
    
    public func syncDelta(snapshotId: String, clientDelta: [Change]) async throws -> DeltaResponse {
        var request = self.request(for: "api/sync/delta")
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = DeltaRequest(snapshotId: snapshotId, clientDelta: clientDelta)
        request.httpBody = try encoder.encode(body)
        
        return try await perform(request)
    }

    public func fetchContent(hash: String) async throws -> Data {
        let request = self.request(for: "api/content/\(hash)")
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError(error: "invalid response")
        }
        if httpResponse.statusCode >= 400 {
            if let error = try? decoder.decode(APIError.self, from: data) {
                throw error
            }
            throw APIError(error: "HTTP \(httpResponse.statusCode)")
        }

        return data
    }
    
    // MARK: - Health
    
    public func healthCheck() async throws -> Bool {
        let request = self.request(for: "health")
        let (_, response) = try await session.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode == 200
    }
}

public struct EmptyResponse: Decodable {}
