//
//  CookbookApiV1.swift
//  Nextcloud Cookbook iOS Client
//
//  Created by Vincent Meilinger on 16.11.23.
//

import Foundation
import OSLog
import UIKit


final class CookbookApiClient: CookbookApiProtocol {
    private let basePath = "/index.php/apps/cookbook/api/v1"
    private let settings: UserSettings

    private struct RecipeImportRequest: Codable {
        let url: String
    }

    init(settings: UserSettings = .shared) {
        self.settings = settings
    }

    // MARK: - Private helpers

    private var auth: String { settings.authString }

    private func makeRequest(
        path: String,
        method: RequestMethod,
        accept: ContentType = .JSON,
        contentType: ContentType? = nil,
        body: Data? = nil
    ) -> ApiRequest {
        var headers = [
            HeaderField.ocsRequest(value: true),
            HeaderField.accept(value: accept)
        ]
        if let contentType = contentType {
            headers.append(HeaderField.contentType(value: contentType))
        }
        return ApiRequest(
            path: basePath + path,
            method: method,
            authString: auth,
            headerFields: headers,
            body: body
        )
    }

    private func sendAndDecode<T: Decodable>(_ request: ApiRequest) async throws -> T {
        let (data, error) = await request.send()
        if let error = error { throw error }
        guard let data = data else { throw NetworkError.unknownError(detail: "No data received") }
        guard let decoded: T = JSONDecoder.safeDecode(data) else {
            throw NetworkError.decodingFailed(detail: "Failed to decode \(T.self)")
        }
        return decoded
    }

    private func sendRaw(_ request: ApiRequest) async throws -> Data {
        let (data, error) = await request.send()
        if let error = error { throw error }
        guard let data = data else { throw NetworkError.unknownError(detail: "No data received") }
        return data
    }

    // MARK: - Protocol implementation

    func importRecipe(url: String) async throws -> RecipeDetail {
        let importRequest = RecipeImportRequest(url: url)
        guard let body = JSONEncoder.safeEncode(importRequest) else {
            throw NetworkError.encodingFailed(detail: "Failed to encode import request")
        }
        let request = makeRequest(path: "/import", method: .POST, contentType: .JSON, body: body)
        return try await sendAndDecode(request)
    }

    func getImage(id: Int, size: RecipeImage.RecipeImageSize) async throws -> UIImage? {
        let imageSize = (size == .FULL ? "full" : "thumb")
        let request = makeRequest(path: "/recipes/\(id)/image?size=\(imageSize)", method: .GET, accept: .IMAGE)
        let data = try await sendRaw(request)
        return UIImage(data: data)
    }

    func getRecipes() async throws -> [Recipe] {
        let request = makeRequest(path: "/recipes", method: .GET)
        return try await sendAndDecode(request)
    }

    func createRecipe(_ recipe: RecipeDetail) async throws -> Int {
        guard let body = JSONEncoder.safeEncode(recipe) else {
            throw NetworkError.encodingFailed(detail: "Failed to encode recipe")
        }
        let request = makeRequest(path: "/recipes", method: .POST, contentType: .JSON, body: body)
        let data = try await sendRaw(request)
        let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
        if let id = json as? Int {
            return id
        }
        throw NetworkError.decodingFailed(detail: "Expected recipe ID in response")
    }

    func getRecipe(id: Int) async throws -> RecipeDetail {
        let request = makeRequest(path: "/recipes/\(id)", method: .GET)
        return try await sendAndDecode(request)
    }

    func updateRecipe(_ recipe: RecipeDetail) async throws -> Int {
        guard let body = JSONEncoder.safeEncode(recipe) else {
            throw NetworkError.encodingFailed(detail: "Failed to encode recipe")
        }
        let request = makeRequest(path: "/recipes/\(recipe.id)", method: .PUT, contentType: .JSON, body: body)
        let data = try await sendRaw(request)
        let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
        if let id = json as? Int {
            return id
        }
        throw NetworkError.decodingFailed(detail: "Expected recipe ID in response")
    }

    func deleteRecipe(id: Int) async throws {
        let request = makeRequest(path: "/recipes/\(id)", method: .DELETE)
        let _ = try await sendRaw(request)
    }

    func getCategories() async throws -> [Category] {
        let request = makeRequest(path: "/categories", method: .GET)
        return try await sendAndDecode(request)
    }

    func getCategory(named categoryName: String) async throws -> [Recipe] {
        let request = makeRequest(path: "/category/\(categoryName)", method: .GET)
        return try await sendAndDecode(request)
    }

    func renameCategory(named categoryName: String, to newName: String) async throws {
        guard let body = JSONEncoder.safeEncode(["name": newName]) else {
            throw NetworkError.encodingFailed(detail: "Failed to encode category name")
        }
        let request = makeRequest(path: "/category/\(categoryName)", method: .PUT, contentType: .JSON, body: body)
        let _ = try await sendRaw(request)
    }

    func getTags() async throws -> [RecipeKeyword] {
        let request = makeRequest(path: "/keywords", method: .GET)
        return try await sendAndDecode(request)
    }

    func getRecipesTagged(keyword: String) async throws -> [Recipe] {
        let request = makeRequest(path: "/tags/\(keyword)", method: .GET)
        return try await sendAndDecode(request)
    }

    func searchRecipes(query: String) async throws -> [Recipe] {
        let request = makeRequest(path: "/search/\(query)", method: .GET)
        return try await sendAndDecode(request)
    }
}
