import Foundation

enum APIError: LocalizedError {
    case invalidResponse
    case http(Int, String)

    var errorDescription: String? {
        switch self {
        case .invalidResponse: return "Invalid server response"
        case .http(let code, let body): return "HTTP \(code): \(body)"
        }
    }
}

final class APIClient {
    static let shared = APIClient()

    private let session: URLSession
    private let decoder = JSONDecoder()
    private let encoder = JSONEncoder()

    private init(session: URLSession = .shared) {
        self.session = session
    }

    func login(email: String, password: String) async throws -> AuthResponse {
        let body = LoginRequest(email: email, password: password, device_name: Config.deviceName)
        let response: AuthResponse = try await send(path: "login", method: "POST", body: body, authenticated: false)
        if let token = response.token {
            KeychainStore.token = token
            KeychainStore.teamRole = response.team_role
        }
        return response
    }

    func logout() async {
        _ = try? await sendEmpty(path: "logout", method: "POST")
        KeychainStore.clear()
    }

    func appointments(from: String, to: String) async throws -> [AppointmentDTO] {
        let response: AppointmentsResponse = try await send(
            path: "job-appointments?from=\(from)&to=\(to)",
            method: "GET"
        )
        return response.data
    }

    func jobs() async throws -> [JobDTO] {
        let response: JobsResponse = try await send(path: "jobs", method: "GET")
        return response.data
    }

    func availableSlots(jobId: Int, date: String) async throws -> [SlotDTO] {
        let response: SlotsResponse = try await send(
            path: "jobs/\(jobId)/available-slots?date=\(date)",
            method: "GET"
        )
        return response.slots
    }

    func book(jobId: Int, startsAt: String) async throws -> AppointmentDTO {
        try await send(
            path: "jobs/\(jobId)/appointments",
            method: "POST",
            body: BookAppointmentRequest(startsAt: startsAt)
        )
    }

    func checkIn(jobId: Int, appointmentId: Int, latitude: Double, longitude: Double) async throws -> CheckInResponse {
        try await send(
            path: "jobs/\(jobId)/appointments/\(appointmentId)/check-in",
            method: "POST",
            body: CheckInRequest(latitude: latitude, longitude: longitude)
        )
    }

    func complete(jobId: Int, appointmentId: Int) async throws -> AppointmentDTO {
        try await send(
            path: "jobs/\(jobId)/appointments/\(appointmentId)/complete",
            method: "POST"
        )
    }

    private func send<T: Decodable>(
        path: String,
        method: String,
        body: (any Encodable)? = nil,
        authenticated: Bool = true
    ) async throws -> T {
        var request = try makeRequest(path: path, method: method, authenticated: authenticated)
        if let body {
            request.httpBody = try encoder.encode(AnyEncodable(body))
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        let (data, response) = try await session.data(for: request)
        try validate(response: response, data: data)
        return try decoder.decode(T.self, from: data)
    }

    private func sendEmpty(path: String, method: String) async throws {
        let request = try makeRequest(path: path, method: method, authenticated: true)
        let (data, response) = try await session.data(for: request)
        try validate(response: response, data: data)
    }

    private func makeRequest(path: String, method: String, authenticated: Bool) throws -> URLRequest {
        let raw = Config.apiBaseURL.absoluteString + path
        guard let finalURL = URL(string: raw) else { throw APIError.invalidResponse }
        var request = URLRequest(url: finalURL)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        if authenticated, let token = KeychainStore.token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        return request
    }

    private func validate(response: URLResponse, data: Data) throws {
        guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
        guard (200..<300).contains(http.statusCode) else {
            let body = String(data: data, encoding: .utf8) ?? ""
            throw APIError.http(http.statusCode, body)
        }
    }
}

private struct AnyEncodable: Encodable {
    private let encodeFunc: (Encoder) throws -> Void

    init(_ wrapped: any Encodable) {
        self.encodeFunc = wrapped.encode
    }

    func encode(to encoder: Encoder) throws {
        try encodeFunc(encoder)
    }
}
