const std = @import("std");

const escaper = @import("../../escaper.zig");

const log = std.log.scoped(.efa);

pub const ApiTransportationProductClass = enum(u32) {
    zug = 0,
    s_bahn = 1,
    u_bahn = 2,
    stadtbahn = 3,
    strassen_trambahn = 4,
    stadtbus = 5,
    regionalbus = 6,
    schnellbus = 7,
    seil_zahnradbahn = 8,
    schiff = 9,
    anruf_sammel_taxi = 10,
    sonstige = 11,
    flugzeug = 12,
    zug_nv = 13,
    zug_fv = 14,
    zug_fv_m_zuschlag = 15,
    zug_fv_m_spez_fpr = 16,
    sev = 17,
    zug_shuttle = 18,
    buergerbus = 19,
    rufbus_liniengebunden = 20,
    rufbus = 21,
    _,

    pub fn toString(self: ApiTransportationProductClass) ?[]const u8 {
        return switch (self) {
            .zug => "Zug",
            .s_bahn => "S-Bahn",
            .u_bahn => "U-Bahn",
            .stadtbahn => "Stadtbahn",
            .strassen_trambahn => "Straßen-/Trambahn",
            .stadtbus => "Stadtbus",
            .regionalbus => "Regionalbus",
            .schnellbus => "Schnellbus",
            .seil_zahnradbahn => "Seil-/Zahnradbahn",
            .schiff => "Schiff",
            .anruf_sammel_taxi => "Anruf-Sammel-Taxi",
            .sonstige => "Sonstige",
            .flugzeug => "Flugzeug",
            .zug_nv => "Zug (Nahverkehr)",
            .zug_fv => "Zug (Fernverkehr)",
            .zug_fv_m_zuschlag => "Zug (Fernverkehr) mit Zuschlag",
            .zug_fv_m_spez_fpr => "Zug (Fernverkehr) mit spezifischer Fpr",
            .sev => "SEV Schnienenersatzverkehr",
            .zug_shuttle => "Zug Shuttle",
            .buergerbus => "Bürgerbus",
            .rufbus_liniengebunden => "Rufbus (liniengebunden)",
            .rufbus => "Rufbus",
            _ => null,
        };
    }
};

pub const ApiLocation = struct {
    id: []const u8,
    name: []const u8,
    properties: struct {
        platform: ?[]const u8 = null,
    },
};

pub const ApiStopEvent = struct {
    location: ApiLocation,
    departureTimePlanned: []const u8,
    departureTimeBaseTimetable: []const u8,
    departureTimeEstimated: ?[]const u8 = null,
    transportation: struct {
        name: []const u8,
        product: struct {
            class: ApiTransportationProductClass,
        },
        operator: ?struct {
            name: []const u8,
        } = null,
        destination: struct {
            name: []const u8,
        },
    },
    hints: ?[]const struct {
        content: []const u8,
        type: []const u8,
    } = null,
    isCancelled: bool = false,
};

pub const DmResponse = struct {
    locations: []const ApiLocation,
    stopEvents: []const ApiStopEvent,
};

pub const Client = struct {
    pub const Error = error{
        InvalidResponse,
    };

    const Self = @This();

    const BodyLimit: usize = 10 * 1024 * 1024;

    allocator: std.mem.Allocator,
    efa_url: []const u8,
    efa_dm_url_str: []const u8,
    efa_stopfinder_url_str: []const u8,

    pub fn init(
        allocator: std.mem.Allocator,
        efa_url: []const u8,
    ) !Self {
        return .{
            .allocator = allocator,
            .efa_url = efa_url,
            .efa_dm_url_str = try std.fmt.allocPrint(
                allocator,
                "{s}/XML_DM_REQUEST?locationServerActive=1&stateless=1&sRaLP=1&itdLPxx_generalInfo=false&mode=direct&type_dm=any&itdLPxx_stopname=false&useRealtime=1&deleteAssignedStops_dm=1&depType=stopEvents&useAllStops=1&outputFormat=rapidJSON",
                .{efa_url},
            ),
            .efa_stopfinder_url_str = try std.fmt.allocPrint(
                allocator,
                "{s}/XML_STOPFINDER_REQUEST?doNotSearchForStops_sf=1&locationInfoActive=0&locationServerActive=0&sl3plusStopFinderMacro=1&type_sf=any&outputFormat=rapidJSON",
                .{efa_url},
            ),
        };
    }

    pub fn deinit(self: *const Self) void {
        self.allocator.free(self.efa_stopfinder_url_str);
        self.allocator.free(self.efa_dm_url_str);
    }

    pub fn findStops(self: *const Self, query: []const u8) !void {
        const escaped_query = try escaper.escapeUriComponent(self.allocator, query);
        defer if (escaped_query) |s| self.allocator.free(s);
        const url_query = escaped_query orelse query;

        var client = std.http.Client{ .allocator = self.allocator };
        defer client.deinit();

        const url_str = try std.fmt.allocPrint(
            self.allocator,
            "{s}&name_sf={s}",
            .{ self.efa_stopfinder_url_str, url_query },
        );
        defer self.allocator.free(url_str);
        const url = try std.Uri.parse(url_str);
        log.info("Finding stops ({s}): {s}", .{ query, url_str });

        var buf: [4096]u8 = undefined;
        var req = try client.open(.GET, url, .{
            .server_header_buffer = &buf,
        });
        defer req.deinit();

        try req.send();
        try req.finish();
        try req.wait();

        if (req.response.status != .ok)
            return Error.InvalidResponse;

        const body = try req.reader().readAllAlloc(self.allocator, BodyLimit);
        defer self.allocator.free(body);

        try std.io.getStdOut().writeAll(body);
    }

    pub fn getDepartures(self: *const Self, stop_id: []const u8) !std.json.Parsed(DmResponse) {
        const escaped_stop_id = try escaper.escapeUriComponent(self.allocator, stop_id);
        defer if (escaped_stop_id) |s| self.allocator.free(s);
        const url_stop_id = escaped_stop_id orelse stop_id;

        var client = std.http.Client{ .allocator = self.allocator };
        defer client.deinit();

        const url_str = try std.fmt.allocPrint(
            self.allocator,
            "{s}&name_dm={s}",
            .{ self.efa_dm_url_str, url_stop_id },
        );
        defer self.allocator.free(url_str);
        const url = try std.Uri.parse(url_str);
        log.info("Fetching departures: {s}", .{url_str});

        var buf: [4096]u8 = undefined;
        var req = try client.open(.GET, url, .{
            .server_header_buffer = &buf,
        });
        defer req.deinit();

        try req.send();
        try req.finish();
        try req.wait();

        if (req.response.status != .ok)
            return Error.InvalidResponse;

        const body = try req.reader().readAllAlloc(self.allocator, BodyLimit);
        defer self.allocator.free(body);

        const response = try std.json.parseFromSlice(
            DmResponse,
            self.allocator,
            body,
            .{ .allocate = .alloc_always, .ignore_unknown_fields = true },
        );

        // const str = try std.json.stringifyAlloc(self.allocator, response.value, .{});
        // defer self.allocator.free(str);
        // try std.io.getStdOut().writeAll(str);

        return response;
    }
};
