import Foundation

public enum Provider: String, Codable, CaseIterable, Hashable, Sendable {
    case openai
    case anthropic
    case xai
    case gemini
    case hermesAgent = "hermes-agent"

    public var displayName: String {
        switch self {
        case .openai: return "OpenAI"
        case .anthropic: return "Anthropic"
        case .xai: return "xAI"
        case .gemini: return "Gemini"
        case .hermesAgent: return "Hermes Agent"
        }
    }
}

public enum MessageRole: String, Codable, Hashable, Sendable {
    case system
    case user
    case assistant
    case tool
}

public struct ChatAttachment: Codable, Hashable, Identifiable, Sendable {
    public enum Kind: String, Codable, Hashable, Sendable {
        case image
        case text
    }

    public var id: String
    public var kind: Kind
    public var filename: String
    public var mimeType: String
    public var sizeBytes: Int
    public var dataUrl: String?
    public var text: String?
    public var truncated: Bool?

    public init(
        id: String,
        kind: Kind,
        filename: String,
        mimeType: String,
        sizeBytes: Int,
        dataUrl: String? = nil,
        text: String? = nil,
        truncated: Bool? = nil
    ) {
        self.id = id
        self.kind = kind
        self.filename = filename
        self.mimeType = mimeType
        self.sizeBytes = sizeBytes
        self.dataUrl = dataUrl
        self.text = text
        self.truncated = truncated
    }

    public static func image(
        id: String = UUID().uuidString,
        filename: String,
        mimeType: String,
        sizeBytes: Int,
        dataUrl: String
    ) -> ChatAttachment {
        ChatAttachment(
            id: id,
            kind: .image,
            filename: filename,
            mimeType: mimeType,
            sizeBytes: sizeBytes,
            dataUrl: dataUrl
        )
    }

    public static func text(
        id: String = UUID().uuidString,
        filename: String,
        mimeType: String,
        sizeBytes: Int,
        text: String,
        truncated: Bool
    ) -> ChatAttachment {
        ChatAttachment(
            id: id,
            kind: .text,
            filename: filename,
            mimeType: mimeType,
            sizeBytes: sizeBytes,
            text: text,
            truncated: truncated
        )
    }

    var jsonValue: JSONValue {
        var object: [String: JSONValue] = [
            "kind": .string(kind.rawValue),
            "id": .string(id),
            "filename": .string(filename),
            "mimeType": .string(mimeType),
            "sizeBytes": .number(Double(sizeBytes))
        ]

        if let dataUrl {
            object["dataUrl"] = .string(dataUrl)
        }
        if let text {
            object["text"] = .string(text)
        }
        if let truncated {
            object["truncated"] = .bool(truncated)
        }

        return .object(object)
    }

    static func attachments(from metadata: JSONValue?) -> [ChatAttachment] {
        guard let metadataObject = metadata?.objectValue,
              let values = metadataObject["attachments"]?.arrayValue
        else {
            return []
        }

        return values.compactMap { value in
            guard let object = value.objectValue,
                  let kindRaw = object["kind"]?.stringValue,
                  let kind = Kind(rawValue: kindRaw),
                  let id = object["id"]?.stringValue,
                  let filename = object["filename"]?.stringValue,
                  let mimeType = object["mimeType"]?.stringValue,
                  let sizeNumber = object["sizeBytes"]?.numberValue
            else {
                return nil
            }

            return ChatAttachment(
                id: id,
                kind: kind,
                filename: filename,
                mimeType: mimeType,
                sizeBytes: Int(sizeNumber),
                dataUrl: object["dataUrl"]?.stringValue,
                text: object["text"]?.stringValue,
                truncated: object["truncated"]?.boolValue
            )
        }
    }
}

public struct ChatSummary: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var title: String?
    public var parentChatId: String? = nil
    public var titleGenerationPending = false
    public var createdAt: Date
    public var updatedAt: Date
    public var starred = false
    public var starredAt: Date?
    public var initiatedProvider: Provider?
    public var initiatedModel: String?
    public var lastUsedProvider: Provider?
    public var lastUsedModel: String?
}

extension ChatSummary {
    private enum CodingKeys: String, CodingKey {
        case id
        case title
        case parentChatId
        case titleGenerationPending
        case createdAt
        case updatedAt
        case starred
        case starredAt
        case initiatedProvider
        case initiatedModel
        case lastUsedProvider
        case lastUsedModel
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decodeIfPresent(String.self, forKey: .title)
        parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
        titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
        createdAt = try container.decode(Date.self, forKey: .createdAt)
        updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
        starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
        initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
        initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
        lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
        lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
    }
}

public struct SearchSummary: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var title: String?
    public var query: String?
    public var createdAt: Date
    public var updatedAt: Date
    public var starred = false
    public var starredAt: Date?
}

public enum WorkspaceItemType: String, Codable, Hashable, Sendable {
    case chat
    case search
}

public struct WorkspaceItem: Codable, Identifiable, Hashable, Sendable {
    public var type: WorkspaceItemType
    public var id: String
    public var title: String?
    public var query: String?
    public var parentChatId: String? = nil
    public var titleGenerationPending = false
    public var createdAt: Date
    public var updatedAt: Date
    public var starred = false
    public var starredAt: Date?
    public var initiatedProvider: Provider?
    public var initiatedModel: String?
    public var lastUsedProvider: Provider?
    public var lastUsedModel: String?

    public init(chat: ChatSummary) {
        self.type = .chat
        self.id = chat.id
        self.title = chat.title
        self.query = nil
        self.parentChatId = chat.parentChatId
        self.titleGenerationPending = chat.titleGenerationPending
        self.createdAt = chat.createdAt
        self.updatedAt = chat.updatedAt
        self.starred = chat.starred
        self.starredAt = chat.starredAt
        self.initiatedProvider = chat.initiatedProvider
        self.initiatedModel = chat.initiatedModel
        self.lastUsedProvider = chat.lastUsedProvider
        self.lastUsedModel = chat.lastUsedModel
    }

    public init(search: SearchSummary) {
        self.type = .search
        self.id = search.id
        self.title = search.title
        self.query = search.query
        self.parentChatId = nil
        self.titleGenerationPending = false
        self.createdAt = search.createdAt
        self.updatedAt = search.updatedAt
        self.starred = search.starred
        self.starredAt = search.starredAt
        self.initiatedProvider = nil
        self.initiatedModel = nil
        self.lastUsedProvider = nil
        self.lastUsedModel = nil
    }

    public var chatSummary: ChatSummary? {
        guard type == .chat else { return nil }
        return ChatSummary(
            id: id,
            title: title,
            parentChatId: parentChatId,
            titleGenerationPending: titleGenerationPending,
            createdAt: createdAt,
            updatedAt: updatedAt,
            starred: starred,
            starredAt: starredAt,
            initiatedProvider: initiatedProvider,
            initiatedModel: initiatedModel,
            lastUsedProvider: lastUsedProvider,
            lastUsedModel: lastUsedModel
        )
    }

    public var searchSummary: SearchSummary? {
        guard type == .search else { return nil }
        return SearchSummary(
            id: id,
            title: title,
            query: query,
            createdAt: createdAt,
            updatedAt: updatedAt,
            starred: starred,
            starredAt: starredAt
        )
    }
}

extension WorkspaceItem {
    private enum CodingKeys: String, CodingKey {
        case type
        case id
        case title
        case query
        case parentChatId
        case titleGenerationPending
        case createdAt
        case updatedAt
        case starred
        case starredAt
        case initiatedProvider
        case initiatedModel
        case lastUsedProvider
        case lastUsedModel
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decode(WorkspaceItemType.self, forKey: .type)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decodeIfPresent(String.self, forKey: .title)
        query = try container.decodeIfPresent(String.self, forKey: .query)
        parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
        titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
        createdAt = try container.decode(Date.self, forKey: .createdAt)
        updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
        starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
        initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
        initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
        lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
        lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
    }
}

public struct Message: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var createdAt: Date
    public var role: MessageRole
    public var content: String
    public var name: String?
    public var metadata: JSONValue? = nil

    public var attachments: [ChatAttachment] {
        ChatAttachment.attachments(from: metadata)
    }

    public var toolCallMetadata: ToolCallMetadata? {
        guard role == .tool,
              let object = metadata?.objectValue,
              object["kind"]?.stringValue == "tool_call"
        else { return nil }

        return ToolCallMetadata(
            toolCallId: object["toolCallId"]?.stringValue,
            toolName: object["toolName"]?.stringValue ?? name,
            status: object["status"]?.stringValue,
            summary: object["summary"]?.stringValue,
            durationMs: object["durationMs"]?.numberValue.map(Int.init),
            error: object["error"]?.stringValue,
            resultPreview: object["resultPreview"]?.stringValue
        )
    }

    public var isToolCallLog: Bool {
        toolCallMetadata != nil
    }
}

public struct ToolCallMetadata: Hashable, Sendable {
    public var toolCallId: String?
    public var toolName: String?
    public var status: String?
    public var summary: String?
    public var durationMs: Int?
    public var error: String?
    public var resultPreview: String?
}

public enum JSONValue: Codable, Hashable, Sendable {
    case string(String)
    case number(Double)
    case bool(Bool)
    case object([String: JSONValue])
    case array([JSONValue])
    case null

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if container.decodeNil() {
            self = .null
            return
        }
        if let value = try? container.decode(Bool.self) {
            self = .bool(value)
            return
        }
        if let value = try? container.decode(Double.self) {
            self = .number(value)
            return
        }
        if let value = try? container.decode(String.self) {
            self = .string(value)
            return
        }
        if let value = try? container.decode([String: JSONValue].self) {
            self = .object(value)
            return
        }
        if let value = try? container.decode([JSONValue].self) {
            self = .array(value)
            return
        }
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case let .string(value):
            try container.encode(value)
        case let .number(value):
            try container.encode(value)
        case let .bool(value):
            try container.encode(value)
        case let .object(value):
            try container.encode(value)
        case let .array(value):
            try container.encode(value)
        case .null:
            try container.encodeNil()
        }
    }

    public var stringValue: String? {
        if case let .string(value) = self {
            return value
        }
        return nil
    }

    public var numberValue: Double? {
        if case let .number(value) = self {
            return value
        }
        return nil
    }

    public var objectValue: [String: JSONValue]? {
        if case let .object(value) = self {
            return value
        }
        return nil
    }

    public var arrayValue: [JSONValue]? {
        if case let .array(value) = self {
            return value
        }
        return nil
    }

    public var boolValue: Bool? {
        if case let .bool(value) = self {
            return value
        }
        return nil
    }
}

public struct ChatDetail: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var title: String?
    public var parentChatId: String? = nil
    public var titleGenerationPending = false
    public var createdAt: Date
    public var updatedAt: Date
    public var starred = false
    public var starredAt: Date?
    public var initiatedProvider: Provider?
    public var initiatedModel: String?
    public var lastUsedProvider: Provider?
    public var lastUsedModel: String?
    public var messages: [Message]
}

extension ChatDetail {
    private enum CodingKeys: String, CodingKey {
        case id
        case title
        case parentChatId
        case titleGenerationPending
        case createdAt
        case updatedAt
        case starred
        case starredAt
        case initiatedProvider
        case initiatedModel
        case lastUsedProvider
        case lastUsedModel
        case messages
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decodeIfPresent(String.self, forKey: .title)
        parentChatId = try container.decodeIfPresent(String.self, forKey: .parentChatId)
        titleGenerationPending = try container.decodeIfPresent(Bool.self, forKey: .titleGenerationPending) ?? false
        createdAt = try container.decode(Date.self, forKey: .createdAt)
        updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        starred = try container.decodeIfPresent(Bool.self, forKey: .starred) ?? false
        starredAt = try container.decodeIfPresent(Date.self, forKey: .starredAt)
        initiatedProvider = try container.decodeIfPresent(Provider.self, forKey: .initiatedProvider)
        initiatedModel = try container.decodeIfPresent(String.self, forKey: .initiatedModel)
        lastUsedProvider = try container.decodeIfPresent(Provider.self, forKey: .lastUsedProvider)
        lastUsedModel = try container.decodeIfPresent(String.self, forKey: .lastUsedModel)
        messages = try container.decode([Message].self, forKey: .messages)
    }
}

public struct SearchResultItem: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var createdAt: Date
    public var rank: Int
    public var title: String?
    public var url: String
    public var publishedDate: String?
    public var author: String?
    public var text: String?
    public var highlights: [String]?
    public var highlightScores: [Double]?
    public var score: Double?
    public var favicon: String?
    public var image: String?
}

public struct SearchCitation: Codable, Hashable, Sendable {
    public var id: String?
    public var url: String?
    public var title: String?
    public var publishedDate: String?
    public var author: String?
    public var text: String?
}

public struct SearchDetail: Codable, Identifiable, Hashable, Sendable {
    public var id: String
    public var title: String?
    public var query: String?
    public var createdAt: Date
    public var updatedAt: Date
    public var starred = false
    public var starredAt: Date?
    public var requestId: String?
    public var latencyMs: Int?
    public var error: String?
    public var answerText: String?
    public var answerRequestId: String?
    public var answerCitations: [SearchCitation]?
    public var answerError: String?
    public var results: [SearchResultItem]
}

public struct ActiveRunsResponse: Codable, Hashable, Sendable {
    public var chats: [String]
    public var searches: [String]

    public init(chats: [String] = [], searches: [String] = []) {
        self.chats = chats
        self.searches = searches
    }
}

public struct SearchRunRequest: Codable, Sendable {
    public var query: String?
    public var title: String?
    public var type: String?
    public var numResults: Int?
    public var includeDomains: [String]?
    public var excludeDomains: [String]?

    public init(
        query: String? = nil,
        title: String? = nil,
        type: String? = nil,
        numResults: Int? = nil,
        includeDomains: [String]? = nil,
        excludeDomains: [String]? = nil
    ) {
        self.query = query
        self.title = title
        self.type = type
        self.numResults = numResults
        self.includeDomains = includeDomains
        self.excludeDomains = excludeDomains
    }
}

public struct CompletionRequestMessage: Codable, Sendable {
    public var role: MessageRole
    public var content: String
    public var name: String?
    public var attachments: [ChatAttachment]?

    public init(role: MessageRole, content: String, name: String? = nil, attachments: [ChatAttachment]? = nil) {
        self.role = role
        self.content = content
        self.name = name
        self.attachments = attachments
    }
}

public struct CompletionStreamMeta: Codable, Sendable {
    public var chatId: String?
    public var callId: String?
    public var provider: Provider
    public var model: String
}

public struct CompletionStreamDelta: Codable, Sendable {
    public var text: String
}

public struct CompletionStreamDone: Codable, Sendable {
    public var text: String
}

public struct CompletionStreamToolCall: Codable, Sendable {
    public var toolCallId: String
    public var name: String
    public var status: String
    public var summary: String
    public var args: [String: JSONValue]
    public var startedAt: String
    public var completedAt: String?
    public var durationMs: Int?
    public var error: String?
    public var resultPreview: String?
}

public struct StreamErrorPayload: Codable, Sendable {
    public var message: String
}

public enum CompletionStreamEvent: Sendable {
    case meta(CompletionStreamMeta)
    case toolCall(CompletionStreamToolCall)
    case delta(CompletionStreamDelta)
    case done(CompletionStreamDone)
    case error(StreamErrorPayload)
    case ignored
}

public struct SearchResultsPayload: Codable, Sendable {
    public var requestId: String?
    public var results: [SearchResultItem]
}

public struct SearchErrorPayload: Codable, Sendable {
    public var error: String
}

public struct SearchAnswerPayload: Codable, Sendable {
    public var answerText: String?
    public var answerRequestId: String?
    public var answerCitations: [SearchCitation]?
}

public struct SearchDonePayload: Codable, Sendable {
    public var search: SearchDetail
}

public enum SearchStreamEvent: Sendable {
    case searchResults(SearchResultsPayload)
    case searchError(SearchErrorPayload)
    case answer(SearchAnswerPayload)
    case answerError(SearchErrorPayload)
    case done(SearchDonePayload)
    case error(StreamErrorPayload)
    case ignored
}

public struct ProviderModelInfo: Codable, Hashable, Sendable {
    public var models: [String]
    public var loadedAt: Date?
    public var error: String?
}

public struct ModelCatalogResponse: Codable, Hashable, Sendable {
    public var providers: [Provider: ProviderModelInfo]

    enum CodingKeys: String, CodingKey {
        case providers
    }

    public init(providers: [Provider: ProviderModelInfo]) {
        self.providers = providers
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let rawProviders = try container.decode([String: ProviderModelInfo].self, forKey: .providers)
        var mapped: [Provider: ProviderModelInfo] = [:]
        for (key, value) in rawProviders {
            if let provider = Provider(rawValue: key) {
                mapped[provider] = value
            }
        }
        self.providers = mapped
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let raw = Dictionary(uniqueKeysWithValues: providers.map { ($0.key.rawValue, $0.value) })
        try container.encode(raw, forKey: .providers)
    }
}

struct AuthSession: Codable {
    var authenticated: Bool
    var mode: String
}

struct ChatListResponse: Codable {
    var chats: [ChatSummary]
}

struct SearchListResponse: Codable {
    var searches: [SearchSummary]
}

struct WorkspaceListResponse: Codable {
    var items: [WorkspaceItem]
}

struct ChatDetailResponse: Codable {
    var chat: ChatDetail
}

struct SearchDetailResponse: Codable {
    var search: SearchDetail
}

struct ChatCreateResponse: Codable {
    var chat: ChatSummary
}

struct SearchCreateResponse: Codable {
    var search: SearchSummary
}

struct DeleteResponse: Codable {
    var deleted: Bool
}

struct SuggestTitleBody: Codable {
    var chatId: String
    var content: String
}

enum APIError: LocalizedError {
    case invalidBaseURL
    case httpError(statusCode: Int, message: String)
    case networkError(message: String)
    case decodingError(message: String)
    case invalidResponse
    case noResponseStream

    var errorDescription: String? {
        switch self {
        case .invalidBaseURL:
            return "Invalid API URL"
        case let .httpError(_, message):
            return message
        case let .networkError(message):
            return message
        case let .decodingError(message):
            return message
        case .invalidResponse:
            return "Unexpected server response"
        case .noResponseStream:
            return "No response stream"
        }
    }
}

extension DateFormatter {
    static let sybilDisplayDate: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = .autoupdatingCurrent
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter
    }()

    static let sybilShortTime: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = .autoupdatingCurrent
        formatter.dateStyle = .none
        formatter.timeStyle = .short
        return formatter
    }()
}

extension Date {
    var sybilRelativeLabel: String {
        let formatter = RelativeDateTimeFormatter()
        formatter.locale = .autoupdatingCurrent
        formatter.unitsStyle = .abbreviated
        return formatter.localizedString(for: self, relativeTo: Date())
    }

    var sybilShortTimeLabel: String {
        DateFormatter.sybilShortTime.string(from: self)
    }
}
