//
//  NavvoError.swift
//  Navvo Maps SDK for iOS
//
//  The typed error surface of the SDK. Every failure is a `NavvoError`, so
//  callers can switch exhaustively:
//
//  ```swift
//  do {
//      let plan = try await NavvoServices.shared.plan(request)
//  } catch let error as NavvoError {
//      switch error {
//      case .scope(let missing, _):        showBanner("Key needs scope: \(missing ?? "?")")
//      case .rateLimit(let retryAfter, _): scheduleRetry(after: retryAfter ?? 2)
//      case .engineUnavailable:            showRoutingDownBanner()
//      default:                            present(error)
//      }
//  }
//  ```
//

import Foundation

/// Context carried by server-originated errors: enough to debug without ever
/// leaking the API key (the SDK only logs the redacted form).
public struct NavvoErrorContext: Sendable {
    /// A human-readable message (from the server body when available).
    public let message: String

    /// HTTP status code, when the error originated from a response.
    public let statusCode: Int?

    /// Request path relative to the base URL, e.g. `/geocode/search`.
    public let path: String?

    /// HTTP method of the failing request.
    public let method: String?

    /// The parsed response body (untyped) for forward-compat debugging.
    public let body: NavvoJSON?

    /// Create an error context.
    public init(
        message: String,
        statusCode: Int? = nil,
        path: String? = nil,
        method: String? = nil,
        body: NavvoJSON? = nil
    ) {
        self.message = message
        self.statusCode = statusCode
        self.path = path
        self.method = method
        self.body = body
    }
}

/// Every error thrown by the Navvo SDK.
public enum NavvoError: Error, Sendable {
    /// Client-side misconfiguration: missing/malformed key, bad option,
    /// invalid argument. Never retryable.
    case config(String)

    /// `401` — no key, or an unknown key.
    case auth(NavvoErrorContext)

    /// `403` — revoked key or a missing scope. `missingScope` carries the
    /// offending scope name when the server enumerates it.
    case scope(missingScope: String?, NavvoErrorContext)

    /// `404` — a genuine not-found. Note that some single-resource lookups
    /// return `200` with a `null` body instead; those surface as `nil`
    /// results, not as this error.
    case notFound(NavvoErrorContext)

    /// `400` / `422` — the server rejected the request as invalid.
    case validation(NavvoErrorContext)

    /// `429`, or `403` quota-throttle — rate limited. `retryAfter` is seconds
    /// from the `Retry-After` header when the server sent one.
    case rateLimit(retryAfter: TimeInterval?, NavvoErrorContext)

    /// `5xx` — an upstream/server error (retryable with backoff).
    case server(NavvoErrorContext)

    /// A routing/engine endpoint answered `200`/`201` with `available: false`
    /// — the engine is down; the HTTP success does not mean a usable result.
    case engineUnavailable(NavvoErrorContext)

    /// A low-level network failure (DNS, connection reset, TLS, offline).
    case network(String)

    /// The request (and any retries) exceeded the per-attempt timeout.
    case timeout(seconds: TimeInterval, attempts: Int)
}

extension NavvoError {
    /// The context of a server-originated error, when present.
    public var context: NavvoErrorContext? {
        switch self {
        case .auth(let ctx),
             .notFound(let ctx),
             .validation(let ctx),
             .server(let ctx),
             .engineUnavailable(let ctx):
            return ctx
        case .scope(_, let ctx):
            return ctx
        case .rateLimit(_, let ctx):
            return ctx
        case .config, .network, .timeout:
            return nil
        }
    }

    /// The HTTP status code, when the error originated from a response.
    public var statusCode: Int? { context?.statusCode }

    /// Whether retrying (with backoff) can plausibly succeed.
    public var isRetryable: Bool {
        switch self {
        case .server, .engineUnavailable, .network, .timeout, .rateLimit:
            return true
        case .config, .auth, .scope, .notFound, .validation:
            return false
        }
    }
}

extension NavvoError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .config(let message):
            return "Navvo configuration error: \(message)"
        case .auth(let ctx):
            return "Navvo auth error (check your API key): \(ctx.message)"
        case .scope(let missing, let ctx):
            if let missing, !missing.isEmpty {
                return "Navvo scope error — key is missing scope '\(missing)': \(ctx.message)"
            }
            return "Navvo scope/forbidden error: \(ctx.message)"
        case .notFound(let ctx):
            return "Navvo not found: \(ctx.message)"
        case .validation(let ctx):
            return "Navvo validation error: \(ctx.message)"
        case .rateLimit(let retryAfter, let ctx):
            if let retryAfter {
                return "Navvo rate limited (retry after \(Int(retryAfter))s): \(ctx.message)"
            }
            return "Navvo rate limited: \(ctx.message)"
        case .server(let ctx):
            return "Navvo server error: \(ctx.message)"
        case .engineUnavailable(let ctx):
            return "Navvo service temporarily unavailable: \(ctx.message)"
        case .network(let message):
            return "Navvo network error: \(message)"
        case .timeout(let seconds, let attempts):
            return "Navvo request timed out after \(seconds)s (\(attempts) attempt(s))"
        }
    }
}
