module util.data;

import handy_http_primitives;
import handy_http_data.multipart;
import std.typecons;
import std.datetime;

Optional!T toOptional(T)(Nullable!T value) {
    if (value.isNull) {
        return Optional!T.empty;
    } else {
        return Optional!T.of(value.get);
    }
}

Nullable!T toNullable(T)(Optional!T value) {
    if (value.isNull) {
        return Nullable!(T)();
    } else {
        return Nullable!T(value.value);
    }
}

T getPathParamOrThrow(T)(in ServerHttpRequest req, string name) {
    import handy_http_handlers.path_handler;
    import std.conv : to, ConvException;
    foreach (param; getPathParams(req)) {
        if (param.name == name) {
            try {
                return to!T(param.value);
            } catch (ConvException e) {
                // Skip and throw if no params match.
            }
        }
    }
    // No params matched, so throw a NOT FOUND error.
    throw new HttpStatusException(HttpStatus.NOT_FOUND, "Missing required path parameter \"" ~ name ~ "\".");
}

struct MultipartFile {
    string name;
    string contentType;
    ubyte[] content;
}

struct MultipartFilesAndBody(T) {
    T payload;
    MultipartFile[] files;
}

MultipartFilesAndBody!T parseMultipartFilesAndBody(T)(
    ref ServerHttpRequest request,
    string payloadPartName = "payload",
    string filesPartName = "file"
) {
    import asdf : deserialize, SerdeException;
    import std.uni : toLower;
    import std.array;

    MultipartFormData formData;
    try {
        formData = readBodyAsMultipartFormData(request);
    } catch (MultipartFormatException e) {
        throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid multipart form data.");
    }

    MultipartFilesAndBody!T result;
    RefAppender!(MultipartFile[]) app = appender(&result.files);
    foreach (element; formData.elements) {
        if (toLower(element.name) == payloadPartName) {
            try {
                result.payload = deserialize!T(element.content);
            } catch (SerdeException e) {
                throw new HttpStatusException(HttpStatus.BAD_REQUEST, e.msg);
            }
        } else if (toLower(element.name) == filesPartName) {
            app ~= parseMultipartFile(element);
        }
    }
    return result;
}

private MultipartFile parseMultipartFile(in MultipartElement e) {
    import std.algorithm : splitter, count;
    import std.array;
    import std.string : strip;
    const requiredHeaders = ["Content-Disposition", "Content-Type"];
    static foreach (headerName; requiredHeaders) {
        if (headerName !in e.headers || e.headers[headerName].length < 1) {
            throw new HttpStatusException(
                HttpStatus.BAD_REQUEST,
                "Missing required \"" ~ headerName ~ "\" header for multipart file."
            );
        }
    }
    if ("Content-Disposition" !in e.headers) {
        throw new HttpStatusException(
            HttpStatus.BAD_REQUEST,
            "Missing required \"Content-Disposition\" header for multipart file."
        );
    }
    string filename;
    foreach (part; e.headers["Content-Disposition"].splitter(";")) {
        string[] partSegments = part.splitter("=").array;
        if (count(partSegments) == 2 && partSegments[0].strip() == "filename") {
            filename = partSegments[1].strip().strip("\"");
            break;
        }
    }
    if (filename is null || filename.length < 1) {
        throw new HttpStatusException(
            HttpStatus.BAD_REQUEST,
            "Missing filename for multipart file."
        );
    }
    return MultipartFile(
        filename,
        e.headers["Content-Type"],
        cast(ubyte[]) e.content
    );
}

/**
 * Struct representing a user-provided time range with optional "from" and "to"
 * timestamps. If both are empty, it is assumed that the user is requesting all
 * data.
 */
struct TimeRange {
    Optional!SysTime fromTime;
    Optional!SysTime toTime;
}

enum ExportFileFormat {
    JSON,
    CSV
}

ExportFileFormat getPreferredExportFileFormat(in ServerHttpRequest request) {
    if ("Accept" in request.headers) {
        string acceptStr = request.getHeaderAs!string("Accept");
        if (acceptStr !is null && acceptStr == ContentTypes.TEXT_CSV) {
            return ExportFileFormat.CSV;
        }
    }
    string fileFormatStr = request.getParamAs!string("file-format");
    if (fileFormatStr !is null && fileFormatStr == ContentTypes.TEXT_CSV) {
        return ExportFileFormat.CSV;
    }
    return ExportFileFormat.JSON; // JSON is the fallback if CSV is not requested.
}

void addFileExportHeaders(
    ref ServerHttpResponse response,
    string filename,
    string contentType,
    bool includeTimestampSuffix = true
) {
    response.headers.add("Content-Type", contentType);
    if (includeTimestampSuffix) {
        import std.format : format;
        import std.string : lastIndexOf;
        SysTime now = Clock.currTime(UTC());
        string timeStr = format!"_%04d-%02d-%02d_%02d-%02d-%02d"(
            now.year, now.month, now.day,
            now.hour, now.minute, now.second
        );
        ptrdiff_t dotIdx = lastIndexOf(filename, '.');
        if (dotIdx == -1) {
            filename = filename ~ timeStr;
        } else {
            filename = filename[0..dotIdx] ~ timeStr ~ filename[dotIdx..$];
        }
    }
    response.headers.add("Content-Disposition", "attachment; filename=" ~ filename);
}
