import Foundation

struct MultipartPart {
    var name: String
    var filename: String?
    var contentType: String?
    var data: Data?

    var textValue: String? {
        guard let data else { return nil }
        return String(data: data, encoding: .utf8)?
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

enum MultipartParser {
    private static let crlf = Data("\r\n".utf8)
    private static let headerSeparator = Data("\r\n\r\n".utf8)
    private static let endMarker = Data("--".utf8)

    static func boundary(from contentType: String) -> String? {
        let parts = contentType.split(separator: ";").map { $0.trimmingCharacters(in: .whitespaces) }
        guard parts.contains(where: { $0.lowercased().hasPrefix("multipart/form-data") }) else {
            return nil
        }
        for part in parts {
            if part.lowercased().hasPrefix("boundary=") {
                var value = String(part.dropFirst("boundary=".count))
                if value.hasPrefix("\"") && value.hasSuffix("\"") {
                    value = String(value.dropFirst().dropLast())
                }
                return value.isEmpty ? nil : value
            }
        }
        return nil
    }

    static func parse(body: Data, boundary: String) -> [MultipartPart] {
        
        guard !body.isEmpty, !boundary.isEmpty else { return [] }
        let delimiter = Data("--\(boundary)".utf8)
        let partsData = split(data: body, delimiter: delimiter)
        var results: [MultipartPart] = []
        for raw in partsData {
            // Reify slices so indices are always 0-based; dropFirst alone keeps non-zero startIndex
            // and makes `subdata(in: 0..<…)` trap with EXC_BREAKPOINT.
            var chunk = Data(raw)
            if chunk.starts(with: crlf) {
                chunk = Data(chunk.dropFirst(2))
            }
            if chunk.isEmpty || chunk.starts(with: endMarker) {
                continue
            }
            guard let headerEnd = chunk.range(of: headerSeparator),
                  let headerData = safeSubdata(chunk, from: chunk.startIndex, to: headerEnd.lowerBound),
                  var content = safeSubdata(chunk, from: headerEnd.upperBound, to: chunk.endIndex)
            else {
                continue
            }
            if content.count >= 2, content.suffix(2) == crlf {
                content = Data(content.dropLast(2))
            }
            guard let headerText = String(data: headerData, encoding: .utf8) else { continue }
            var name = ""
            var filename: String?
            var contentType: String?
            for line in headerText.split(separator: "\r\n") {
                let lower = line.lowercased()
                if lower.hasPrefix("content-disposition:") {
                    name = attribute(named: "name", in: String(line)) ?? ""
                    filename = attribute(named: "filename", in: String(line))
                } else if lower.hasPrefix("content-type:") {
                    contentType = line.split(separator: ":", maxSplits: 1).last?
                        .trimmingCharacters(in: .whitespaces)
                }
            }
            results.append(
                MultipartPart(
                    name: name,
                    filename: filename,
                    contentType: contentType,
                    data: content
                )
            )
        }
        return results
    }

    private static func attribute(named key: String, in line: String) -> String? {
        let pattern = "\(key)=\""
        guard let range = line.range(of: pattern) else { return nil }
        let rest = line[range.upperBound...]
        guard let end = rest.firstIndex(of: "\"") else { return nil }
        return String(rest[..<end])
    }

    private static func split(data: Data, delimiter: Data) -> [Data] {
        guard !delimiter.isEmpty else { return data.isEmpty ? [] : [data] }
        var result: [Data] = []
        var searchRange = data.startIndex..<data.endIndex
        while let found = data.range(of: delimiter, in: searchRange) {
            if let piece = safeSubdata(data, from: searchRange.lowerBound, to: found.lowerBound),
               !piece.isEmpty {
                result.append(piece)
            }
            searchRange = found.upperBound..<data.endIndex
        }
        if let tail = safeSubdata(data, from: searchRange.lowerBound, to: searchRange.upperBound),
           !tail.isEmpty {
            result.append(tail)
        }
        return result
    }

    /// Bounds-checked slice; returns nil instead of trapping on invalid ranges.
    private static func safeSubdata(_ data: Data, from lower: Data.Index, to upper: Data.Index) -> Data? {
        guard lower >= data.startIndex,
              upper <= data.endIndex,
              lower <= upper
        else {
            return nil
        }
        return data.subdata(in: lower..<upper)
    }
}
