//
//  APIClient.swift
//  suixinkan
//
//  Created by Codex on 2026/6/18.
//

import Foundation
import Observation

/// URLSession 抽象协议，用于让网络客户端支持测试替身。
protocol URLSessionProtocol {
    /// 发起 URLRequest 并返回原始数据和响应。
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
}

extension URLSession: URLSessionProtocol {}

@MainActor
@Observable
/// 统一网络请求客户端，负责构造请求、注入 token、校验响应和解析业务 Envelope。
final class APIClient {
    @ObservationIgnored private let session: URLSessionProtocol
    @ObservationIgnored private let encoder: JSONEncoder
    @ObservationIgnored private let decoder: JSONDecoder
    @ObservationIgnored private var authTokenProvider: (() -> String?)?

    private let environment: APIEnvironment
    private let appVersion: String
    private let osType: String

    /// 初始化网络客户端及其编码、解码和环境配置。
    init(
        environment: APIEnvironment = .current,
        session: URLSessionProtocol = APIClient.defaultSession,
        encoder: JSONEncoder = JSONEncoder(),
        decoder: JSONDecoder = JSONDecoder(),
        appVersion: String = AppClientInfo.appVersion(),
        osType: String = AppClientInfo.osType
    ) {
        self.environment = environment
        self.session = session
        self.encoder = encoder
        self.decoder = decoder
        self.appVersion = appVersion.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "1.0.0"
        self.osType = osType.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? AppClientInfo.osType
    }

    nonisolated private static let defaultSession: URLSession = {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 12
        configuration.timeoutIntervalForResource = 20
        configuration.waitsForConnectivity = false
        return URLSession(configuration: configuration)
    }()

    /// 绑定 token 提供者，让业务 API 不需要直接持有登录状态。
    func bindAuthTokenProvider(_ provider: @escaping () -> String?) {
        authTokenProvider = provider
    }

    /// 发送强类型 APIRequest，并返回解包后的业务数据。
    func send<Response: Decodable>(
        _ apiRequest: APIRequest<Response>,
        tokenOverride: String? = nil
    ) async throws -> Response {
        let request = try makeURLRequest(apiRequest, tokenOverride: tokenOverride)
        logRequest(request)

        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.data(for: request)
        } catch is CancellationError {
            logCancelled(for: request, reason: "CancellationError")
            throw CancellationError()
        } catch let error as URLError {
            if error.code == .cancelled {
                logCancelled(for: request, reason: "URLError.cancelled")
                throw CancellationError()
            }
            throw APIError.networkFailed(networkErrorMessage(for: error))
        } catch {
            throw APIError.networkFailed(error.localizedDescription)
        }

        logResponse(for: request, response: response, data: data)
        try validateHTTPResponse(response, data: data)
        return try decodeEnvelope(Response.self, from: data)
    }

    /// 将业务请求模型转换为 URLRequest，并注入公共 Header、token 和请求体。
    private func makeURLRequest<Response: Decodable>(
        _ apiRequest: APIRequest<Response>,
        tokenOverride: String?
    ) throws -> URLRequest {
        let path = apiRequest.path.hasPrefix("/") ? apiRequest.path : "/" + apiRequest.path
        guard var components = URLComponents(
            string: environment.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + path
        ) else {
            throw APIError.invalidURL
        }

        if !apiRequest.queryItems.isEmpty {
            components.queryItems = apiRequest.queryItems
        }

        guard let url = components.url else {
            throw APIError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = apiRequest.method.rawValue
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue(appVersion, forHTTPHeaderField: "X-APP-VERSION")
        request.setValue(osType, forHTTPHeaderField: "X-OS-TYPE")

        apiRequest.headers.forEach { key, value in
            request.setValue(value, forHTTPHeaderField: key)
        }

        let token = tokenOverride ?? authTokenProvider?()
        if let token = token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
            request.setValue(token, forHTTPHeaderField: "token")
        }

        if let body = apiRequest.body {
            request.httpBody = try encoder.encode(body)
        }

        return request
    }

    /// 校验 HTTP 层响应状态码，非 2xx 时提取错误信息。
    private func validateHTTPResponse(_ response: URLResponse, data: Data) throws {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }

        guard 200 ..< 300 ~= httpResponse.statusCode else {
            throw APIError.httpStatus(httpResponse.statusCode, parseHTTPErrorMessage(data: data))
        }
    }

    /// 解码后端统一 Envelope，并返回内部 data 数据。
    private func decodeEnvelope<Response: Decodable>(_ responseType: Response.Type, from data: Data) throws -> Response {
        let envelope: APIEnvelope<Response>
        do {
            envelope = try decoder.decode(APIEnvelope<Response>.self, from: data)
        } catch {
            throw APIError.decodeFailed(error.localizedDescription)
        }

        guard envelope.isSuccess else {
            throw APIError.serverCode(envelope.code, envelope.msg ?? "业务请求失败")
        }

        if responseType == EmptyPayload.self {
            return EmptyPayload() as! Response
        }

        guard let payload = envelope.data else {
            throw APIError.emptyData
        }

        return payload
    }

    /// 从 HTTP 错误响应中提取更适合展示给用户的错误信息。
    private func parseHTTPErrorMessage(data: Data) -> String {
        if let envelope = try? decoder.decode(ErrorEnvelope.self, from: data) {
            if let msg = envelope.msg?.trimmingCharacters(in: .whitespacesAndNewlines), !msg.isEmpty {
                return msg
            }
            if let message = envelope.message?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty {
                return message
            }
            if let error = envelope.error?.trimmingCharacters(in: .whitespacesAndNewlines), !error.isEmpty {
                return error
            }
        }

        if let plainText = String(data: data, encoding: .utf8)?
            .trimmingCharacters(in: .whitespacesAndNewlines),
           !plainText.isEmpty {
            return plainText.count > 120 ? String(plainText.prefix(120)) + "..." : plainText
        }

        return "服务端返回错误"
    }

    /// 将 URLError 转换成中文网络错误提示。
    private func networkErrorMessage(for error: URLError) -> String {
        switch error.code {
        case .timedOut:
            "请求超时，请稍后重试"
        case .notConnectedToInternet:
            "网络不可用，请检查网络连接"
        case .networkConnectionLost:
            "网络连接中断，请重试"
        case .cannotFindHost, .cannotConnectToHost, .dnsLookupFailed:
            "无法连接服务器，请稍后重试"
        default:
            error.localizedDescription
        }
    }

    /// 在 Debug 环境打印请求信息。
    private func logRequest(_ request: URLRequest) {
        #if DEBUG
        let method = request.httpMethod ?? "REQUEST"
        let url = request.url?.absoluteString ?? "<invalid url>"
        print("[API][Request] \(method) \(url)")
        #endif
    }

    /// 在 Debug 环境打印响应状态和响应体。
    private func logResponse(for request: URLRequest, response: URLResponse, data: Data) {
        #if DEBUG
        let method = request.httpMethod ?? "REQUEST"
        let url = request.url?.absoluteString ?? "<invalid url>"
        let statusCode = (response as? HTTPURLResponse).map { String($0.statusCode) } ?? "unknown"
        let body = Self.debugResponseBody(from: data)
        print("[API][Response] \(method) \(url) status=\(statusCode)\n\(body)")
        #endif
    }

    /// 在 Debug 环境打印被取消的请求信息。
    private func logCancelled(for request: URLRequest, reason: String) {
        #if DEBUG
        let method = request.httpMethod ?? "REQUEST"
        let url = request.url?.absoluteString ?? "<invalid url>"
        print("[API][Cancelled] \(method) \(url) reason=\(reason)")
        #endif
    }

    #if DEBUG
    /// 将响应体格式化为便于调试阅读的字符串。
    private static func debugResponseBody(from data: Data) -> String {
        guard !data.isEmpty else { return "<empty response>" }
        if
            let object = try? JSONSerialization.jsonObject(with: data),
            let prettyData = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]),
            let prettyJSON = String(data: prettyData, encoding: .utf8) {
            return prettyJSON
        }
        return String(data: data, encoding: .utf8) ?? "<non-utf8 response: \(data.count) bytes>"
    }
    #endif
}

private extension String {
    var nonEmpty: String? {
        isEmpty ? nil : self
    }
}
