//
//  OpenRouterModels.swift
//  Confab
//
//  OpenRouter API request and response models
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import Foundation

// MARK: - API Request

struct ReasoningAPIConfig: Codable {
    let effort: String
    let exclude: Bool?

    enum CodingKeys: String, CodingKey {
        case effort
        case exclude
    }
}

struct OpenRouterChatRequest: Codable {
    let model: String
    let messages: [APIMessage]
    var stream: Bool
    let maxTokens: Int?
    let temperature: Double?
    let topP: Double?
    let tools: [Tool]?
    let toolChoice: String?
    let modalities: [String]?
    let reasoning: ReasoningAPIConfig?
    let cacheControl: CacheControl?

    struct CacheControl: Codable {
        let type: String
    }

    struct APIMessage: Codable {
        let role: String
        let content: MessageContent
        
        enum MessageContent: Codable {
            case string(String)
            case array([ContentItem])
            
            init(from decoder: Decoder) throws {
                let container = try decoder.singleValueContainer()
                if let str = try? container.decode(String.self) {
                    self = .string(str)
                } else if let arr = try? container.decode([ContentItem].self) {
                    self = .array(arr)
                } else {
                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid content")
                }
            }
            
            func encode(to encoder: Encoder) throws {
                var container = encoder.singleValueContainer()
                switch self {
                case .string(let str):
                    try container.encode(str)
                case .array(let arr):
                    try container.encode(arr)
                }
            }
        }
        
        enum ContentItem: Codable {
            case text(String)
            case image(ImageContent)

            struct TextContent: Codable {
                let type: String // "text"
                let text: String
            }

            struct ImageContent: Codable {
                let type: String // "image_url"
                let imageUrl: ImageURL

                struct ImageURL: Codable {
                    let url: String
                }

                enum CodingKeys: String, CodingKey {
                    case type
                    case imageUrl = "image_url"
                }
            }

            init(from decoder: Decoder) throws {
                let container = try decoder.singleValueContainer()
                if let textContent = try? container.decode(TextContent.self), textContent.type == "text" {
                    self = .text(textContent.text)
                } else if let image = try? container.decode(ImageContent.self) {
                    self = .image(image)
                } else if let str = try? container.decode(String.self) {
                    self = .text(str)
                } else {
                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid content item")
                }
            }

            func encode(to encoder: Encoder) throws {
                var container = encoder.singleValueContainer()
                switch self {
                case .text(let text):
                    try container.encode(TextContent(type: "text", text: text))
                case .image(let image):
                    try container.encode(image)
                }
            }
        }
    }
    
    enum CodingKeys: String, CodingKey {
        case model
        case messages
        case stream
        case maxTokens = "max_tokens"
        case temperature
        case topP = "top_p"
        case tools
        case toolChoice = "tool_choice"
        case modalities
        case reasoning
        case cacheControl = "cache_control"
    }
}

// MARK: - API Response

struct OpenRouterChatResponse: Codable {
    let id: String
    let model: String
    let choices: [Choice]
    let usage: Usage?
    let created: Int
    
    struct Choice: Codable {
        let index: Int
        let message: MessageContent
        let finishReason: String?

        struct MessageContent: Codable {
            let role: String
            let content: String?
            let toolCalls: [APIToolCall]?
            let images: [ImageOutput]?
            // Images extracted from content[] blocks (e.g. GPT-5 Image response format)
            let contentBlockImages: [ImageOutput]

            private struct ContentBlock: Codable {
                let type: String
                let text: String?
                let imageUrl: ImageOutput.ImageURL?
                enum CodingKeys: String, CodingKey {
                    case type, text
                    case imageUrl = "image_url"
                }
            }

            enum CodingKeys: String, CodingKey {
                case role
                case content
                case toolCalls = "tool_calls"
                case images
            }

            init(from decoder: Decoder) throws {
                let c = try decoder.container(keyedBy: CodingKeys.self)
                role = try c.decode(String.self, forKey: .role)
                toolCalls = try c.decodeIfPresent([APIToolCall].self, forKey: .toolCalls)
                images = try c.decodeIfPresent([ImageOutput].self, forKey: .images)
                // content can be a plain String OR an array of content blocks
                if let text = try? c.decodeIfPresent(String.self, forKey: .content) {
                    content = text
                    contentBlockImages = []
                } else if let blocks = try? c.decodeIfPresent([ContentBlock].self, forKey: .content) {
                    content = blocks.compactMap { $0.text }.joined().nonEmptyOrNil
                    contentBlockImages = blocks.compactMap { block in
                        guard block.type == "image_url", let url = block.imageUrl else { return nil }
                        return ImageOutput(imageUrl: url)
                    }
                } else {
                    content = nil
                    contentBlockImages = []
                }
            }
        }

        enum CodingKeys: String, CodingKey {
            case index
            case message
            case finishReason = "finish_reason"
        }
    }

    struct ImageOutput: Codable {
        let imageUrl: ImageURL

        struct ImageURL: Codable {
            let url: String
        }

        enum CodingKeys: String, CodingKey {
            case imageUrl = "image_url"
        }
    }
    
    struct Usage: Codable {
        let promptTokens: Int
        let completionTokens: Int
        let totalTokens: Int
        let promptTokensDetails: PromptTokensDetails?

        struct PromptTokensDetails: Codable {
            let cachedTokens: Int?
            let cacheWriteTokens: Int?

            enum CodingKeys: String, CodingKey {
                case cachedTokens = "cached_tokens"
                case cacheWriteTokens = "cache_write_tokens"
            }
        }

        enum CodingKeys: String, CodingKey {
            case promptTokens = "prompt_tokens"
            case completionTokens = "completion_tokens"
            case totalTokens = "total_tokens"
            case promptTokensDetails = "prompt_tokens_details"
        }
    }
}

// MARK: - Streaming Response

struct OpenRouterStreamChunk: Codable {
    let id: String
    let model: String
    let choices: [StreamChoice]
    let usage: OpenRouterChatResponse.Usage?
    
    struct StreamChoice: Codable {
        let index: Int
        let delta: Delta
        let finishReason: String?

        struct Delta: Codable {
            let role: String?
            let content: String?
            let reasoning: String?
            // images[] from top-level delta field (custom OpenRouter format)
            let images: [OpenRouterChatResponse.ImageOutput]?
            // images extracted from content[] array (standard OpenAI content-block format)
            let contentBlockImages: [OpenRouterChatResponse.ImageOutput]

            private struct ContentBlock: Codable {
                let type: String
                let text: String?
                let imageUrl: OpenRouterChatResponse.ImageOutput.ImageURL?
                enum CodingKeys: String, CodingKey {
                    case type, text
                    case imageUrl = "image_url"
                }
            }

            enum CodingKeys: String, CodingKey {
                case role, content, images, reasoning
            }

            init(from decoder: Decoder) throws {
                let c = try decoder.container(keyedBy: CodingKeys.self)
                role = try c.decodeIfPresent(String.self, forKey: .role)
                images = try c.decodeIfPresent([OpenRouterChatResponse.ImageOutput].self, forKey: .images)
                reasoning = try c.decodeIfPresent(String.self, forKey: .reasoning)
                // content can be a plain String OR an array of content blocks
                if let text = try? c.decodeIfPresent(String.self, forKey: .content) {
                    content = text
                    contentBlockImages = []
                } else if let blocks = try? c.decodeIfPresent([ContentBlock].self, forKey: .content) {
                    content = blocks.compactMap { $0.text }.joined().nonEmptyOrNil
                    contentBlockImages = blocks.compactMap { block in
                        guard block.type == "image_url", let url = block.imageUrl else { return nil }
                        return OpenRouterChatResponse.ImageOutput(imageUrl: url)
                    }
                } else {
                    content = nil
                    contentBlockImages = []
                }
            }
        }

        enum CodingKeys: String, CodingKey {
            case index
            case delta
            case finishReason = "finish_reason"
        }
    }
}

// MARK: - Models List

struct OpenRouterModelsResponse: Codable {
    let data: [ModelData]
    
    struct ModelData: Codable {
        let id: String
        let name: String
        let description: String?
        let contextLength: Int
        let pricing: PricingData
        let architecture: Architecture?
        let supportedParameters: [String]?
        let outputModalities: [String]?
        let created: Int?

        struct PricingData: Codable {
            let prompt: String
            let completion: String
        }

        struct Architecture: Codable {
            let modality: String?
            let tokenizer: String?
            let instructType: String?

            enum CodingKeys: String, CodingKey {
                case modality
                case tokenizer
                case instructType = "instruct_type"
            }
        }

        enum CodingKeys: String, CodingKey {
            case id
            case name
            case description
            case contextLength = "context_length"
            case pricing
            case architecture
            case supportedParameters = "supported_parameters"
            case outputModalities = "output_modalities"
            case created
        }
    }
}

// MARK: - Credits Response

struct OpenRouterCreditsResponse: Codable {
    let data: CreditsData

    struct CreditsData: Codable {
        let totalCredits: Double?
        let totalUsage: Double?

        enum CodingKeys: String, CodingKey {
            case totalCredits = "total_credits"
            case totalUsage = "total_usage"
        }
    }
}

// MARK: - Tool Call Models

struct APIToolCall: Codable {
    let id: String
    let type: String
    let function: FunctionCall

    struct FunctionCall: Codable {
        let name: String
        let arguments: String
    }
}

/// Message shape for encoding assistant messages that contain tool calls
struct AssistantToolCallMessage: Encodable {
    let role: String
    let content: String?
    let toolCalls: [APIToolCall]

    enum CodingKeys: String, CodingKey {
        case role
        case content
        case toolCalls = "tool_calls"
    }
}

/// Message shape for encoding tool result messages back to the API
struct ToolResultMessage: Encodable {
    let role: String // "tool"
    let toolCallId: String
    let name: String
    let content: String

    enum CodingKeys: String, CodingKey {
        case role
        case toolCallId = "tool_call_id"
        case name
        case content
    }
}

// MARK: - Images API — Model Discovery

struct OpenRouterImageModelsResponse: Codable {
    let data: [ImageModelData]

    struct ImageModelData: Codable {
        let id: String
        let name: String
        let description: String?
        let architecture: Architecture?
        let supportsStreaming: Bool?

        struct Architecture: Codable {
            let inputModalities: [String]?
            let outputModalities: [String]?

            enum CodingKeys: String, CodingKey {
                case inputModalities = "input_modalities"
                case outputModalities = "output_modalities"
            }
        }

        enum CodingKeys: String, CodingKey {
            case id, name, description, architecture
            case supportsStreaming = "supports_streaming"
        }
    }
}

// MARK: - Images API — Generation Response

struct OpenRouterImageGenerationResponse: Codable {
    let created: Int?
    let data: [ImageData]
    let usage: Usage?

    struct ImageData: Codable {
        let b64Json: String
        let mediaType: String?

        enum CodingKeys: String, CodingKey {
            case b64Json = "b64_json"
            case mediaType = "media_type"
        }
    }

    struct Usage: Codable {
        let promptTokens: Int
        let completionTokens: Int
        let totalTokens: Int
        let cost: Double?

        enum CodingKeys: String, CodingKey {
            case promptTokens = "prompt_tokens"
            case completionTokens = "completion_tokens"
            case totalTokens = "total_tokens"
            case cost
        }
    }
}

// MARK: - Error Response

struct OpenRouterErrorResponse: Codable {
    let error: ErrorDetail
    
    struct ErrorDetail: Codable {
        let message: String
        let type: String?
        let code: String?
    }
}
