// RemoteDSProvider.swift — S2 구현 (원격 DS 코퍼스 RAG).
//
// S1 계약과 만나는 다리 (CONTRACT.md §4 / AI-ROUTING.md §4):
//   complete(corpusAsk) → DSAskClient.ask(query:backend:) → AskResponse → AICompletionResponse
//   AskResponse.ai_answer        → text
//   AskResponse.citations[]      → [AICitation]
//   AskResponse.synthesis_status → AIFinishReason
//   AskResponse.confidence       → AIConfidence
//   AskResponse.backend_used     → routingNote (어느 LLM 이 응답했는지)
//
// HTTP 는 S3 의 구체 client(LiveDSClient)가 소유 — S2 는 DSAskClient 프로토콜 seam + 매핑만.
// 인터페이스 동결: AIProvider 프로토콜은 불변. RemoteDSProvider.init(client:) 은 S2 가 채우는 구현부.
import Foundation

// MARK: - S2 가 소유하는 DS ask seam (구체 impl = S3)

/// DS `GET /search/ask?q=&backend=` 호출 추상화. S3 의 LiveDSClient 가 conform,
/// S2 는 mock 으로 단위테스트(라이브 네트워크 0). HTTP 실패는 conformer 가 throw
/// (권장: `AIProviderError.backendError(.remoteDS, status:, reason:)`) — 침묵 폴백 금지.
public protocol DSAskClient: Sendable {
    func ask(query: String, backend: String) async throws -> AskResponse
}

// MARK: - DS /search/ask 응답 (부분 미러, 디코딩 전용)
//
// 명시 CodingKeys — convertFromSnakeCase 금지(S3 모델 규약과 일관). fixture: contract/fixtures/ask.json.

public struct AskResponse: Decodable, Sendable {
    public let aiAnswer: String
    public let citations: [AskCitation]
    public let synthesisStatus: String
    public let synthesisMs: Double?
    public let confidence: String?
    public let backendUsed: String?
    public let refused: Bool?

    enum CodingKeys: String, CodingKey {
        case aiAnswer = "ai_answer"
        case citations
        case synthesisStatus = "synthesis_status"
        case synthesisMs = "synthesis_ms"
        case confidence
        case backendUsed = "backend_used"
        case refused
    }

    public init(aiAnswer: String, citations: [AskCitation], synthesisStatus: String,
                synthesisMs: Double? = nil, confidence: String? = nil,
                backendUsed: String? = nil, refused: Bool? = nil) {
        self.aiAnswer = aiAnswer
        self.citations = citations
        self.synthesisStatus = synthesisStatus
        self.synthesisMs = synthesisMs
        self.confidence = confidence
        self.backendUsed = backendUsed
        self.refused = refused
    }
}

public struct AskCitation: Decodable, Sendable {
    public let n: Int
    public let docId: Int
    public let title: String?
    public let sectionTitle: String?
    public let spanText: String

    enum CodingKeys: String, CodingKey {
        case n
        case docId = "doc_id"
        case title
        case sectionTitle = "section_title"
        case spanText = "span_text"
    }

    public init(n: Int, docId: Int, title: String?, sectionTitle: String?, spanText: String) {
        self.n = n
        self.docId = docId
        self.title = title
        self.sectionTitle = sectionTitle
        self.spanText = spanText
    }
}

// MARK: - Provider

public struct RemoteDSProvider: AIProvider {
    public let id: AIProviderID = .remoteDS
    private let client: DSAskClient

    public init(client: DSAskClient) {
        self.client = client
    }

    /// 원격 코퍼스는 항상 후보(라우팅 시맨틱). 실제 도달 실패는 complete 에서 표면화.
    public var isAvailable: Bool {
        get async { true }
    }

    public func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse {
        // corpusAsk 외 태스크는 이 provider 의 책임 아님(라우터가 거름).
        guard request.task == .corpusAsk else {
            throw AIProviderError.notImplemented(id)
        }
        try Task.checkCancellation()
        let backend = Self.dsBackend(for: request.explicitProvider)
        // HTTP 실패(503 등)는 client 가 throw → 그대로 전파(자동 로컬 폴백 금지).
        let response = try await client.ask(query: request.prompt, backend: backend)
        return Self.map(response)
    }

    // MARK: 매핑 (AI-ROUTING.md §4, 고정)

    static func map(_ r: AskResponse) -> AICompletionResponse {
        let citations = r.citations.map {
            AICitation(n: $0.n, docId: $0.docId, title: $0.title,
                       sectionTitle: $0.sectionTitle, spanText: $0.spanText)
        }
        return AICompletionResponse(
            text: r.aiAnswer,
            providerUsed: .remoteDS,
            finishReason: finishReason(fromSynthesisStatus: r.synthesisStatus),
            citations: citations,
            confidence: r.confidence.flatMap(AIConfidence.init(rawValue:)),
            latencyMs: r.synthesisMs,            // latency 는 synthesis_ms 만 기록(하드 게이트 없음)
            routingNote: r.backendUsed           // 어느 LLM 이 응답했는지
        )
    }

    static func finishReason(fromSynthesisStatus status: String) -> AIFinishReason {
        switch status {
        case "completed": return .completed
        case "timeout": return .timeout
        case "no_evidence", "skipped": return .noEvidence
        case "backend_unavailable": return .unavailable
        default: return .refused
        }
    }

    /// explicitProvider → DS 합성 backend (AI-ROUTING.md §4, 고정).
    /// **dict 아닌 exhaustive switch** — 미래 AIProviderID 추가 시 컴파일러가 backend 결정을 강제
    /// (미매핑 provider → nil → 미정의 backend → 404 침묵실패를 컴파일 타임에 차단).
    static func dsBackend(for explicit: AIProviderID?) -> String {
        guard let explicit else { return "mac-mini-default" }   // 미지정 → DS 기본
        switch explicit {
        case .localMLX:    return "gemma-macmini"
        case .remoteDS:    return "mac-mini-default"   // 명시 remoteDS = DS 기본 합성
        case .onDevice:    return "mac-mini-default"   // onDevice 는 코퍼스 합성 불가 → DS 기본
        case .specialized: return "mac-mini-default"   // specialized 코퍼스 backend 없음 → DS 기본
        }
        // TODO(qwen-macbook): 현재 어떤 AIProviderID 도 'qwen-macbook'(M5 Max Qwen VLM) 로 매핑 안 됨.
        //   해당 provider case 가 생기면 위 exhaustive switch 가 컴파일 실패 → backend 결정 강제(S2-1b 게이트 b).
        // TODO(claude-cloud): cloud backend = 'claude-cloud' 는 DS 가 503(scaffold, S2-4b). 매핑하는 case 없음.
        //   503 은 client 가 backendError(.remoteDS, status:503, …) 로 표면화 — 절대 로컬 침묵 폴백 X(과금 버킷 분리).
    }
}
