import Foundation
import UniformTypeIdentifiers
import UIKit

enum ChatAttachmentError: LocalizedError {
    case unsupportedType(String)
    case imageTooLarge(String)
    case textTooLarge(String)
    case unreadableFile(String)
    case unsupportedImageFormat(String)
    case tooManyAttachments(Int)

    var errorDescription: String? {
        switch self {
        case let .unsupportedType(filename):
            return "Unsupported file type for '\(filename)'. Use PNG/JPEG images or text-based files."
        case let .imageTooLarge(filename):
            return "Image '\(filename)' exceeds the 6 MB upload limit."
        case let .textTooLarge(filename):
            return "Text file '\(filename)' exceeds the 8 MB upload limit."
        case let .unreadableFile(filename):
            return "Could not read '\(filename)'."
        case let .unsupportedImageFormat(filename):
            return "Image '\(filename)' could not be converted to PNG or JPEG."
        case let .tooManyAttachments(limit):
            return "You can attach up to \(limit) files per message."
        }
    }
}

enum SybilChatAttachmentSupport {
    static let maxAttachmentsPerMessage = 8
    static let maxImageBytes = 6 * 1024 * 1024
    static let maxTextBytes = 8 * 1024 * 1024
    static let maxTextCharacters = 200_000

    private static let supportedTextExtensions: Set<String> = [
        "txt", "md", "markdown", "csv", "tsv", "json", "jsonl", "xml", "yaml", "yml", "html", "htm",
        "css", "js", "jsx", "ts", "tsx", "py", "rb", "java", "c", "cc", "cpp", "h", "hpp", "go",
        "rs", "sh", "sql", "log", "toml", "ini", "cfg", "conf", "swift", "kt", "m", "mm"
    ]

    private static let supportedTextMimeTypes: Set<String> = [
        "application/json",
        "application/ld+json",
        "application/sql",
        "application/toml",
        "application/x-httpd-php",
        "application/x-javascript",
        "application/x-sh",
        "application/xml",
        "application/yaml",
        "application/x-yaml",
        "image/svg+xml"
    ]

    static func attachmentSummary(_ attachments: [ChatAttachment]) -> String {
        guard !attachments.isEmpty else { return "" }
        let names = attachments.map(\.filename).joined(separator: ", ")
        return attachments.count == 1 ? names : "Attached: \(names)"
    }

    static func metadataValue(for attachments: [ChatAttachment]) -> JSONValue? {
        guard !attachments.isEmpty else { return nil }
        return .object([
            "attachments": .array(attachments.map(\.jsonValue))
        ])
    }

    static func buildAttachments(from urls: [URL]) throws -> [ChatAttachment] {
        try urls.map { try buildAttachment(fromFileURL: $0) }
    }

    static func buildImageAttachment(image: UIImage, filename: String = "pasted-image.jpg") throws -> ChatAttachment {
        if let pngData = image.pngData(), pngData.count <= maxImageBytes {
            return try buildImageAttachment(data: pngData, filename: filename, contentType: .png)
        }

        guard let jpegData = image.jpegData(compressionQuality: 0.92) else {
            throw ChatAttachmentError.unsupportedImageFormat(filename)
        }

        return try buildImageAttachment(data: jpegData, filename: filename, contentType: .jpeg)
    }

    static func buildTextAttachment(text: String, filename: String = "pasted-text.txt", mimeType: String = "text/plain") throws -> ChatAttachment {
        let data = Data(text.utf8)
        return try buildTextAttachment(data: data, filename: filename, mimeType: mimeType)
    }

    @MainActor
    static func buildAttachments(from itemProviders: [NSItemProvider]) async throws -> [ChatAttachment] {
        var attachments: [ChatAttachment] = []

        for provider in itemProviders {
            if let fileURL = try await loadFileURL(from: provider) {
                attachments.append(try buildAttachment(fromFileURL: fileURL))
                continue
            }

            if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
                if let attachment = try await loadImageAttachment(from: provider) {
                    attachments.append(attachment)
                }
            }
        }

        return attachments
    }

    static func previewText(for attachment: ChatAttachment) -> String {
        let normalized = (attachment.text ?? "")
            .replacingOccurrences(of: "\r", with: "")
            .trimmingCharacters(in: .whitespacesAndNewlines)

        if normalized.isEmpty {
            return "(empty file)"
        }

        if normalized.count <= 280 {
            return normalized
        }

        let endIndex = normalized.index(normalized.startIndex, offsetBy: 280)
        return normalized[..<endIndex].trimmingCharacters(in: .whitespacesAndNewlines) + "..."
    }

    static func image(for attachment: ChatAttachment) -> UIImage? {
        guard attachment.kind == .image,
              let dataURL = attachment.dataUrl,
              let data = decodeDataURL(dataURL)
        else {
            return nil
        }
        return UIImage(data: data)
    }

    private static func buildAttachment(fromFileURL url: URL) throws -> ChatAttachment {
        let accessed = url.startAccessingSecurityScopedResource()
        defer {
            if accessed {
                url.stopAccessingSecurityScopedResource()
            }
        }

        let filename = url.lastPathComponent.isEmpty ? "attachment" : url.lastPathComponent
        let resourceValues = try? url.resourceValues(forKeys: [.contentTypeKey])
        let contentType = resourceValues?.contentType ?? UTType(filenameExtension: url.pathExtension)

        let data: Data
        do {
            data = try Data(contentsOf: url)
        } catch {
            throw ChatAttachmentError.unreadableFile(filename)
        }

        if contentType?.conforms(to: .image) == true {
            return try buildImageAttachment(data: data, filename: filename, contentType: contentType)
        }

        if isTextLike(contentType: contentType, mimeType: contentType?.preferredMIMEType, filename: filename) {
            return try buildTextAttachment(data: data, filename: filename, mimeType: contentType?.preferredMIMEType ?? "text/plain")
        }

        throw ChatAttachmentError.unsupportedType(filename)
    }

    static func buildImageAttachment(data: Data, filename: String, contentType: UTType?) throws -> ChatAttachment {
        var mimeType = contentType?.preferredMIMEType
        var payload = data

        if mimeType != "image/png" && mimeType != "image/jpeg" {
            guard let image = UIImage(data: data) else {
                throw ChatAttachmentError.unsupportedImageFormat(filename)
            }

            if let pngData = image.pngData(), pngData.count <= maxImageBytes {
                payload = pngData
                mimeType = "image/png"
            } else if let jpegData = image.jpegData(compressionQuality: 0.92) {
                payload = jpegData
                mimeType = "image/jpeg"
            } else {
                throw ChatAttachmentError.unsupportedImageFormat(filename)
            }
        }

        if payload.count > maxImageBytes {
            throw ChatAttachmentError.imageTooLarge(filename)
        }

        let normalizedMimeType = (mimeType == "image/png") ? "image/png" : "image/jpeg"
        let dataUrl = "data:\(normalizedMimeType);base64,\(payload.base64EncodedString())"

        return .image(
            filename: filename,
            mimeType: normalizedMimeType,
            sizeBytes: payload.count,
            dataUrl: dataUrl
        )
    }

    private static func buildTextAttachment(data: Data, filename: String, mimeType: String) throws -> ChatAttachment {
        if data.count > maxTextBytes {
            throw ChatAttachmentError.textTooLarge(filename)
        }

        let normalized = String(decoding: data, as: UTF8.self)
            .replacingOccurrences(of: "\r\n", with: "\n")
            .replacingOccurrences(of: "\u{0000}", with: "")

        let truncated = normalized.count > maxTextCharacters
        let trimmedText: String
        if truncated {
            let endIndex = normalized.index(normalized.startIndex, offsetBy: maxTextCharacters)
            trimmedText = String(normalized[..<endIndex])
        } else {
            trimmedText = normalized
        }

        return .text(
            filename: filename,
            mimeType: mimeType,
            sizeBytes: data.count,
            text: trimmedText,
            truncated: truncated
        )
    }

    private static func isTextLike(contentType: UTType?, mimeType: String?, filename: String) -> Bool {
        if let contentType {
            if contentType.conforms(to: .text) || contentType.conforms(to: .plainText) || contentType.conforms(to: .sourceCode) {
                return true
            }
            if contentType.conforms(to: .json) || contentType.conforms(to: .xml) {
                return true
            }
        }

        if let mimeType {
            if mimeType.hasPrefix("text/") {
                return true
            }
            if supportedTextMimeTypes.contains(mimeType.lowercased()) {
                return true
            }
        }

        let ext = URL(fileURLWithPath: filename).pathExtension.lowercased()
        return supportedTextExtensions.contains(ext)
    }

    private static func decodeDataURL(_ value: String) -> Data? {
        guard let separator = value.firstIndex(of: ",") else {
            return nil
        }

        let encoded = value[value.index(after: separator)...]
        return Data(base64Encoded: String(encoded))
    }

    @MainActor
    private static func loadFileURL(from provider: NSItemProvider) async throws -> URL? {
        guard provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) else {
            return nil
        }

        return try await withCheckedThrowingContinuation { continuation in
            provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                if let url = item as? URL {
                    continuation.resume(returning: url)
                    return
                }

                if let data = item as? Data,
                   let url = URL(dataRepresentation: data, relativeTo: nil) {
                    continuation.resume(returning: url)
                    return
                }

                if let string = item as? String,
                   let url = URL(string: string) {
                    continuation.resume(returning: url)
                    return
                }

                continuation.resume(returning: nil)
            }
        }
    }

    @MainActor
    private static func loadImageAttachment(from provider: NSItemProvider) async throws -> ChatAttachment? {
        let preferredImageType: UTType = if provider.hasItemConformingToTypeIdentifier(UTType.png.identifier) {
            .png
        } else if provider.hasItemConformingToTypeIdentifier(UTType.jpeg.identifier) {
            .jpeg
        } else {
            .image
        }

        if let data = try await loadDataRepresentation(from: provider, type: preferredImageType) {
            let filenameExtension = preferredImageType.preferredFilenameExtension ?? "jpg"
            let filename = "pasted-image.\(filenameExtension)"
            return try buildImageAttachment(data: data, filename: filename, contentType: preferredImageType)
        }

        if let image = try await loadUIImage(from: provider),
           let jpegData = image.jpegData(compressionQuality: 0.92) {
            return try buildImageAttachment(data: jpegData, filename: "pasted-image.jpg", contentType: .jpeg)
        }

        return nil
    }

    @MainActor
    private static func loadDataRepresentation(from provider: NSItemProvider, type: UTType) async throws -> Data? {
        guard provider.hasItemConformingToTypeIdentifier(type.identifier) else {
            return nil
        }

        return try await withCheckedThrowingContinuation { continuation in
            provider.loadDataRepresentation(forTypeIdentifier: type.identifier) { data, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                continuation.resume(returning: data)
            }
        }
    }

    @MainActor
    private static func loadUIImage(from provider: NSItemProvider) async throws -> UIImage? {
        guard provider.canLoadObject(ofClass: UIImage.self) else {
            return nil
        }

        return try await withCheckedThrowingContinuation { continuation in
            provider.loadObject(ofClass: UIImage.self) { object, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                continuation.resume(returning: object as? UIImage)
            }
        }
    }
}
