//! HTMLImportManifest generates JSON manifests for HTML imports in Bun's bundler.
//!
//! When you import an HTML file in JavaScript:
//! ```javascript
//! import index from "./index.html";
//! console.log(index);
//! ```
//!
//! Bun transforms this into a call to `__jsonParse()` with a JSON manifest containing
//! metadata about all the files generated from the HTML import:
//!
//! ```javascript
//! var src_default = __jsonParse(
//!   '{"index":"./index.html","files":[{"input":"index.html","path":"./index-f2me3qnf.js","loader":"js","isEntry":true,"headers":{"etag": "eet6gn75","content-type": "text/javascript;charset=utf-8"}},{"input":"index.html","path":"./index.html","loader":"html","isEntry":true,"headers":{"etag": "r9njjakd","content-type": "text/html;charset=utf-8"}},{"input":"index.html","path":"./index-gysa5fmk.css","loader":"css","isEntry":true,"headers":{"etag": "50zb7x61","content-type": "text/css;charset=utf-8"}},{"input":"logo.svg","path":"./logo-kygw735p.svg","loader":"file","isEntry":false,"headers":{"etag": "kygw735p","content-type": "application/octet-stream"}},{"input":"react.svg","path":"./react-ck11dneg.svg","loader":"file","isEntry":false,"headers":{"etag": "ck11dneg","content-type": "application/octet-stream"}}]}'
//! );
//! ```
//!
//! The manifest JSON structure contains:
//! - `index`: The original HTML file path
//! - `files`: Array of all generated files with metadata:
//!   - `input`: Original source file path
//!   - `path`: Generated output file path (with content hash)
//!   - `loader`: File type/loader used (js, css, html, file, etc.)
//!   - `isEntry`: Whether this file is an entry point
//!   - `headers`: HTTP headers including ETag and Content-Type
//!
//! This enables applications to:
//! 1. Know all files generated from an HTML import
//! 2. Get proper MIME types and ETags for serving files
//! 3. Implement proper caching strategies
//! 4. Handle assets referenced by the HTML file
//!
//! The manifest is generated during the linking phase and serialized as a JSON string
//! that gets embedded directly into the JavaScript output.
const HTMLImportManifest = @This();

index: u32,
graph: *const Graph,
chunks: []Chunk,
linker_graph: *const LinkerGraph,

pub fn format(this: HTMLImportManifest, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) bun.OOM!void {
    return write(this.index, this.graph, this.linker_graph, this.chunks, writer) catch |err| switch (err) {
        // We use std.fmt.count for this
        error.NoSpaceLeft => unreachable,
        error.OutOfMemory => return error.OutOfMemory,
        else => unreachable,
    };
}

fn writeEntryItem(
    writer: anytype,
    input: []const u8,
    path: []const u8,
    hash: u64,
    loader: options.Loader,
    kind: bun.JSC.API.BuildArtifact.OutputKind,
) !void {
    try writer.writeAll("{");

    if (input.len > 0) {
        try writer.writeAll("\"input\":");
        try bun.js_printer.writeJSONString(input, @TypeOf(writer), writer, .utf8);
        try writer.writeAll(",");
    }

    try writer.writeAll("\"path\":");
    try bun.js_printer.writeJSONString(path, @TypeOf(writer), writer, .utf8);

    try writer.writeAll(",\"loader\":\"");
    try writer.writeAll(@tagName(loader));
    try writer.writeAll("\",\"isEntry\":");
    try writer.writeAll(if (kind == .@"entry-point") "true" else "false");
    try writer.writeAll(",\"headers\":{");

    if (hash > 0) {
        var base64_buf: [bun.base64.encodeLenFromSize(@sizeOf(@TypeOf(hash))) + 2]u8 = undefined;
        const base64 = base64_buf[0..bun.base64.encodeURLSafe(&base64_buf, &std.mem.toBytes(hash))];
        try writer.print(
            \\"etag":"{s}",
        , .{base64});
    }

    try writer.print(
        \\"content-type":"{s}"
    , .{
        // Valid mime types are valid headers, which do not need to be escaped in JSON.
        loader.toMimeType(&.{
            path,
        }).value,
    });

    try writer.writeAll("}}");
}

// Extremely unfortunate, but necessary due to E.String not accepting pre-rescaped input and this happening at the very end.
pub fn writeEscapedJSON(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, chunks: []const Chunk, writer: anytype) !void {
    var stack = std.heap.stackFallback(4096, bun.default_allocator);
    const allocator = stack.get();
    var bytes = std.ArrayList(u8).init(allocator);
    defer bytes.deinit();
    try write(index, graph, linker_graph, chunks, bytes.writer());
    try bun.js_printer.writePreQuotedString(bytes.items, @TypeOf(writer), writer, '"', false, true, .utf8);
}

fn escapedJSONFormatter(this: HTMLImportManifest, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) bun.OOM!void {
    return writeEscapedJSON(this.index, this.graph, this.linker_graph, this.chunks, writer) catch |err| switch (err) {
        // We use std.fmt.count for this
        error.NoSpaceLeft => unreachable,
        error.OutOfMemory => return error.OutOfMemory,
        else => unreachable,
    };
}

pub fn formatEscapedJSON(this: HTMLImportManifest) std.fmt.Formatter(escapedJSONFormatter) {
    return std.fmt.Formatter(escapedJSONFormatter){ .data = this };
}

pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, chunks: []const Chunk, writer: anytype) !void {
    const browser_source_index = graph.html_imports.html_source_indices.slice()[index];
    const server_source_index = graph.html_imports.server_source_indices.slice()[index];
    const sources: []const bun.logger.Source = graph.input_files.items(.source);
    const bv2: *const BundleV2 = @alignCast(@fieldParentPtr("graph", graph));
    var entry_point_bits = try bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, graph.entry_points.items.len);
    defer entry_point_bits.deinit(bun.default_allocator);

    const root_dir = if (bv2.transpiler.options.root_dir.len > 0) bv2.transpiler.options.root_dir else bun.fs.FileSystem.instance.top_level_dir;

    try writer.writeAll("{");

    for (chunks) |*ch| {
        if (ch.entry_point.source_index == browser_source_index and ch.entry_point.is_entry_point) {
            entry_point_bits.set(ch.entry_point.entry_point_id);

            if (ch.content == .html) {
                try writer.writeAll("\"index\":");
                try bun.js_printer.writeJSONString(ch.final_rel_path, @TypeOf(writer), writer, .utf8);
                try writer.writeAll(",");
            }
        }
    }

    // Start the files array

    try writer.writeAll("\"files\":[");

    var first = true;

    const additional_output_files = graph.additional_output_files.items;
    const file_entry_bits: []const AutoBitSet = linker_graph.files.items(.entry_bits);
    var already_visited_output_file = try bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, additional_output_files.len);
    defer already_visited_output_file.deinit(bun.default_allocator);

    // Write all chunks that have files associated with this entry point.
    for (chunks) |*ch| {
        if (ch.entryBits().hasIntersection(&entry_point_bits)) {
            if (!first) try writer.writeAll(",");
            first = false;

            try writeEntryItem(
                writer,
                brk: {
                    if (!ch.entry_point.is_entry_point) break :brk "";
                    var path_for_key = bun.path.relativeNormalized(
                        root_dir,
                        sources[ch.entry_point.source_index].path.text,
                        .posix,
                        false,
                    );
                    if (path_for_key.len > 2 and strings.eqlComptime(path_for_key[0..2], "./")) {
                        path_for_key = path_for_key[2..];
                    }

                    break :brk path_for_key;
                },
                ch.final_rel_path,
                ch.isolated_hash,
                ch.content.loader(),
                if (ch.entry_point.is_entry_point)
                    .@"entry-point"
                else
                    .chunk,
            );
        }
    }

    for (additional_output_files, 0..) |*output_file, i| {
        // Only print the file once.
        if (already_visited_output_file.isSet(i)) continue;

        if (output_file.source_index.unwrap()) |source_index| {
            if (source_index.get() == server_source_index) continue;
            const bits: *const AutoBitSet = &file_entry_bits[source_index.get()];

            if (bits.hasIntersection(&entry_point_bits)) {
                already_visited_output_file.set(i);
                if (!first) try writer.writeAll(",");
                first = false;

                var path_for_key = bun.path.relativeNormalized(
                    root_dir,
                    sources[source_index.get()].path.text,
                    .posix,
                    false,
                );
                if (path_for_key.len > 2 and strings.eqlComptime(path_for_key[0..2], "./")) {
                    path_for_key = path_for_key[2..];
                }

                try writeEntryItem(
                    writer,
                    path_for_key,
                    output_file.dest_path,
                    output_file.hash,
                    output_file.loader,
                    output_file.output_kind,
                );
            }
        }
    }

    try writer.writeAll("]}");
}

const bun = @import("bun");
const strings = bun.strings;
const default_allocator = bun.default_allocator;

const std = @import("std");
const options = @import("../options.zig");

const Loader = options.Loader;
const AutoBitSet = bun.bit_set.AutoBitSet;
const bundler = bun.bundle_v2;
const BundleV2 = bundler.BundleV2;
const Graph = bundler.Graph;
const LinkerGraph = bundler.LinkerGraph;

const Chunk = bundler.Chunk;
