//! Analysis feature container. Renders hub statistics for rules, members,
//! models, and attestation activity with local focus state.

const std = @import("std");
const vaxis = @import("vaxis");
const vxfw = vaxis.vxfw;
const theme = @import("../../theme.zig");
const w = @import("../../widgets.zig");
const data = @import("../../models/view_types.zig");

pub const Focus = enum { chart, rules, members, inputs };

pub const State = struct {
    scope_idx: usize = 0,
    breathing_phase: u8 = 0,
    focus: Focus = .inputs,
    rule_cursor: usize = 0,
    member_cursor: usize = 0,
    input_cursor: usize = 0,
    expanded_rule: ?usize = null,
    show_member_detail: bool = false,
};

pub fn drawRoot(
    self: anytype,
    ctx: vxfw.DrawContext,
    insights: *const data.AnalysisData,
    available: bool,
) std.mem.Allocator.Error!vxfw.Surface {
    const size = ctx.max.size();
    var root = try vxfw.Surface.init(ctx.arena, self.widget(), size);
    w.fillSurface(&root, theme.CANVAS);

    const members_w: u16 = 18;
    const rules_w: u16 = size.width -| members_w;
    const children = try ctx.arena.alloc(vxfw.SubSurface, 2);

    const main_surface = if (self.analysis.show_member_detail)
        try drawMemberDetail(self, ctx, rules_w, size.height, insights)
    else
        try drawRules(self, ctx, rules_w, size.height, insights, available);
    children[0] = .{ .origin = .{ .row = 0, .col = 0 }, .surface = main_surface };
    children[1] = .{
        .origin = .{ .row = 0, .col = rules_w },
        .surface = try drawMembers(self, ctx, members_w, size.height, insights, available),
    };

    root.children = children;
    return root;
}

pub fn drawRules(
    self: anytype,
    ctx: vxfw.DrawContext,
    width: u16,
    height: u16,
    insights: *const data.AnalysisData,
    available: bool,
) std.mem.Allocator.Error!vxfw.Surface {
    const border_color = theme.focusBorder(self.analysis.focus == .rules);
    var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height });
    w.fillSurface(&surface, theme.PANEL);
    w.drawBorder(&surface, border_color, theme.PANEL);

    w.writeText(&surface, ctx, 3, 0, "Rules Rank", theme.boldOn(theme.PANEL, theme.TEXT));
    const col_refs: u16 = width -| 18;
    const col_move: u16 = width -| 9;
    w.writeText(&surface, ctx, col_refs, 0, "refs", theme.fg(theme.MUTED));
    w.writeText(&surface, ctx, col_move, 0, "move", theme.fg(theme.MUTED));

    if (!available) {
        w.writeText(&surface, ctx, 2, 1, "Remote analysis unavailable.", theme.fg(theme.MUTED));
        return surface;
    }

    var ranked_rules: std.ArrayList(data.AnalysisRule) = .empty;
    for (insights.rules) |rule| {
        if (rule.refer_count == 0) continue;
        try ranked_rules.append(ctx.arena, rule);
    }
    const rules = ranked_rules.items;

    if (rules.len == 0) {
        w.writeText(&surface, ctx, 2, 1, "No rule analysis data returned by the hub.", theme.fg(theme.MUTED));
        return surface;
    }
    if (self.analysis.rule_cursor >= rules.len) self.analysis.rule_cursor = rules.len - 1;
    if (self.analysis.expanded_rule) |expanded_idx| {
        if (expanded_idx >= rules.len) self.analysis.expanded_rule = null;
    }

    var max_refer: u32 = 1;
    for (rules) |rule| {
        if (rule.refer_count > max_refer) max_refer = rule.refer_count;
    }

    const name_col: u16 = 2;
    const min_name_w: u16 = 18;
    const max_name_w: u16 = @max(min_name_w, @min(@as(u16, 40), col_refs -| name_col -| 12));
    var measured_name_w: u16 = min_name_w;
    for (rules) |rule| {
        const candidate = @as(u16, @intCast(ctx.stringWidth(firstLineTrimmed(rule.name, max_name_w))));
        if (candidate > measured_name_w) measured_name_w = candidate;
    }
    const name_w: u16 = @min(max_name_w, measured_name_w);
    const bar_start: u16 = name_col + name_w + 1;
    const bar_end: u16 = col_refs -| 2;
    const bar_max_w: u16 = bar_end -| bar_start;

    const focused = self.analysis.focus == .rules;
    var row: u16 = 1;
    for (rules, 0..) |rule, rule_idx| {
        if (row >= height -| 1) break;
        const is_sel = rule_idx == self.analysis.rule_cursor and focused;

        if (is_sel) {
            w.writeCursorMarker(&surface, 1, row);
        }

        const name_style: vaxis.Style = if (is_sel)
            theme.boldOn(theme.PANEL, theme.TEXT)
        else
            theme.fg(theme.TEXT_SOFT);
        w.writeText(&surface, ctx, name_col, row, firstLineTrimmed(rule.name, name_w), name_style);

        const bar_w_raw: u16 = @intCast(@as(u32, bar_max_w) * rule.refer_count / max_refer);
        const bar_w: u16 = if (rule.refer_count > 0 and bar_w_raw == 0 and bar_max_w > 0) 1 else bar_w_raw;
        for (0..bar_w) |offset| {
            const bc: u16 = bar_start + @as(u16, @intCast(offset));
            if (bc >= bar_end) break;
            const t: f32 = if (bar_w > 1)
                @as(f32, @floatFromInt(offset)) / @as(f32, @floatFromInt(bar_w - 1))
            else
                0.5;
            surface.writeCell(bc, row, .{
                .char = .{ .grapheme = "\xe2\x96\x88", .width = 1 },
                .style = .{ .fg = theme.lerpColor(theme.ACCENT_SOFT, theme.ACCENT, t), .bg = theme.PANEL },
            });
        }
        const ref_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{rule.refer_count});
        w.writeText(&surface, ctx, col_refs, row, ref_text, theme.fg(theme.TEXT_SOFT));

        const rank_move = try ruleRankMove(ctx.arena, rules, rule_idx);
        const move_style: vaxis.Style = switch (rank_move.kind) {
            .up => theme.fg(theme.OK),
            .down => theme.fg(theme.DANGER),
            .flat => theme.fg(theme.MUTED),
            .none => theme.fg(theme.DIM),
        };
        w.writeText(&surface, ctx, col_move, row, rank_move.text, move_style);
        row += 1;

        if (self.analysis.expanded_rule == rule_idx) {
            var constraint_max: u32 = 1;
            for (rule.constraints) |constraint| {
                if (constraint.refer_count > constraint_max) constraint_max = constraint.refer_count;
            }
            for (rule.constraints, 0..) |constraint, constraint_idx| {
                if (row >= height -| 1) break;
                const is_constraint_idle = constraint.refer_count == 0;
                const is_last = constraint_idx + 1 == rule.constraints.len;
                const show_child_id = !std.mem.eql(u8, constraint.id, constraint.label);
                const child_label_col: u16 = if (show_child_id) 12 else 9;
                const child_label = firstLineTrimmed(constraint.label, bar_start -| child_label_col -| 2);
                const child_bar_start: u16 = bar_start + 2;
                const tree_char: []const u8 = if (is_last) "\xe2\x94\x94" else "\xe2\x94\x9c";
                w.writeText(&surface, ctx, 5, row, tree_char, theme.fg(theme.DIM));
                if (show_child_id) {
                    w.writeText(&surface, ctx, 7, row, firstLineTrimmed(constraint.id, 4), theme.fg(theme.MUTED));
                }
                w.writeText(
                    &surface,
                    ctx,
                    child_label_col,
                    row,
                    child_label,
                    if (is_constraint_idle) theme.fg(theme.DANGER) else theme.fg(theme.TEXT_SOFT),
                );
                if (!is_constraint_idle) {
                    const child_bar_max_w: u16 = bar_max_w / 2;
                    const c_bar_w_raw: u16 = @intCast(@min(
                        @as(u32, child_bar_max_w) * constraint.refer_count / constraint_max,
                        child_bar_max_w,
                    ));
                    const c_bar_w: u16 = if (constraint.refer_count > 0 and c_bar_w_raw == 0 and child_bar_max_w > 0) 1 else c_bar_w_raw;
                    for (0..c_bar_w) |offset| {
                        const bc: u16 = child_bar_start + @as(u16, @intCast(offset));
                        if (bc >= col_refs -| 2) break;
                        const t: f32 = if (c_bar_w > 1)
                            @as(f32, @floatFromInt(offset)) / @as(f32, @floatFromInt(c_bar_w - 1))
                        else
                            0.5;
                        surface.writeCell(bc, row, .{
                            .char = .{ .grapheme = "\xe2\x96\x88", .width = 1 },
                            .style = .{ .fg = theme.lerpColor(theme.ACCENT_SOFT, theme.ACCENT, t), .bg = theme.PANEL },
                        });
                    }
                    const c_ref = try std.fmt.allocPrint(ctx.arena, " {d}", .{constraint.refer_count});
                    w.writeText(&surface, ctx, col_refs, row, c_ref, theme.fg(theme.MUTED));
                } else {
                    const idle_txt = if (constraint.idle_days) |days|
                        try std.fmt.allocPrint(ctx.arena, "idle {d}d \xe2\x9a\xa0", .{days})
                    else
                        "idle \xe2\x9a\xa0";
                    w.writeText(&surface, ctx, col_refs, row, idle_txt, .{ .fg = theme.DANGER, .bg = theme.PANEL });
                }
                row += 1;
            }
        }
    }

    return surface;
}

pub fn drawMembers(
    self: anytype,
    ctx: vxfw.DrawContext,
    width: u16,
    height: u16,
    insights: *const data.AnalysisData,
    available: bool,
) std.mem.Allocator.Error!vxfw.Surface {
    const border_color = theme.focusBorder(self.analysis.focus == .members);
    var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height });
    w.fillSurface(&surface, theme.PANEL);
    w.drawBorder(&surface, border_color, theme.PANEL);

    w.writeText(&surface, ctx, 2, 0, "User Activity", theme.boldOn(theme.PANEL, theme.TEXT));

    if (!available) {
        w.writeText(&surface, ctx, 2, 1, "No", theme.fg(theme.MUTED));
        w.writeText(&surface, ctx, 2, 2, "stats", theme.fg(theme.MUTED));
        return surface;
    }

    if (insights.members.len == 0) {
        w.writeText(&surface, ctx, 2, 1, "No", theme.fg(theme.MUTED));
        w.writeText(&surface, ctx, 2, 2, "users", theme.fg(theme.MUTED));
        return surface;
    }

    var member_max: u16 = 1;
    for (insights.members) |member| {
        for (member.trend) |value| {
            if (value > member_max) member_max = value;
        }
    }

    var row: u16 = 1;
    const num_weeks: u16 = (30 + 6) / 7;
    const grid_col: u16 = 2;

    for (insights.members, 0..) |member, member_idx| {
        if (row + 8 >= height) break;
        const is_sel = member_idx == self.analysis.member_cursor and self.analysis.focus == .members;
        if (is_sel) {
            w.writeCursorMarker(&surface, 1, row);
        }
        const name_style = if (is_sel) theme.boldOn(theme.PANEL, theme.TEXT) else theme.fg(theme.TEXT_SOFT);
        w.writeText(&surface, ctx, 2, row, member.username, name_style);
        row += 1;

        var week: u16 = 0;
        while (week < num_weeks) : (week += 1) {
            if (row >= height -| 1) break;
            var day: u16 = 0;
            while (day < 7) : (day += 1) {
                const data_idx = week * 7 + day;
                if (data_idx >= 30) break;
                const value = member.trend[data_idx];
                const max_f: f32 = @floatFromInt(@max(member_max, 1));
                const fg = if (value == 0)
                    theme.rgb(0xffe8b8)
                else
                    theme.lerpColor(theme.ACCENT_SOFT, theme.ACCENT, @as(f32, @floatFromInt(value)) / max_f);
                const col = grid_col + day * 2;
                if (col < width -| 1) {
                    surface.writeCell(col, row, .{
                        .char = .{ .grapheme = "\xe2\x96\xa0", .width = 1 },
                        .style = .{ .fg = fg, .bg = theme.PANEL },
                    });
                }
            }
            row += 1;
        }
        row += 1;
    }

    return surface;
}

pub fn drawMemberDetail(
    self: anytype,
    ctx: vxfw.DrawContext,
    width: u16,
    height: u16,
    insights: *const data.AnalysisData,
) std.mem.Allocator.Error!vxfw.Surface {
    if (insights.members.len == 0) {
        var empty_surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height });
        w.fillSurface(&empty_surface, theme.PANEL);
        w.drawBorder(&empty_surface, theme.BORDER, theme.PANEL);
        w.writeText(&empty_surface, ctx, 2, 1, "No member data available.", theme.fg(theme.MUTED));
        return empty_surface;
    }

    const member_idx = @min(self.analysis.member_cursor, insights.members.len - 1);
    const member = &insights.members[member_idx];
    var surface = try vxfw.Surface.init(ctx.arena, self.widget(), .{ .width = width, .height = height });
    w.fillSurface(&surface, theme.PANEL);
    w.drawBorder(&surface, theme.ACCENT, theme.PANEL);

    const title = try std.fmt.allocPrint(ctx.arena, "{s}'s Activity", .{member.username});
    w.writeText(&surface, ctx, 3, 0, title, theme.boldOn(theme.PANEL, theme.TEXT));
    w.writeRightText(&surface, ctx, 0, "Esc back", theme.fg(theme.MUTED));

    var row: u16 = 2;
    const summary = try std.fmt.allocPrint(ctx.arena, "{d} refs  {d}d active", .{ member.refer_count, member.active_days });
    w.writeText(&surface, ctx, 3, row, summary, theme.fg(theme.TEXT));
    row += 2;

    w.writeText(&surface, ctx, 3, row, "Top Rules", theme.fgBold(theme.TEXT));
    row += 1;
    var max_ref: u32 = 1;
    for (member.top_rules) |top_rule| {
        if (top_rule.refer_count > max_ref) max_ref = top_rule.refer_count;
    }
    const name_col: u16 = 5;
    const count_col: u16 = width -| 7;
    const min_name_w: u16 = 14;
    const max_name_w: u16 = @max(min_name_w, @min(@as(u16, 32), count_col -| name_col -| 12));
    var measured_name_w: u16 = min_name_w;
    for (member.top_rules) |top_rule| {
        const candidate = @as(u16, @intCast(ctx.stringWidth(firstLineTrimmed(top_rule.name, max_name_w))));
        if (candidate > measured_name_w) measured_name_w = candidate;
    }
    const name_w: u16 = @min(max_name_w, measured_name_w);
    const bar_start: u16 = name_col + name_w + 1;
    const bar_end: u16 = count_col -| 1;
    const bar_max_w: u16 = bar_end -| bar_start;
    for (member.top_rules) |top_rule| {
        if (row >= height -| 5) break;
        w.writeText(&surface, ctx, name_col, row, firstLineTrimmed(top_rule.name, name_w), theme.fg(theme.TEXT_SOFT));
        const bar_w_raw: u16 = @intCast(@as(u32, bar_max_w) * top_rule.refer_count / max_ref);
        const bar_w: u16 = if (top_rule.refer_count > 0 and bar_w_raw == 0 and bar_max_w > 0) 1 else bar_w_raw;
        for (0..bar_w) |offset| {
            const bc: u16 = bar_start + @as(u16, @intCast(offset));
            if (bc >= bar_end) break;
            const t: f32 = if (bar_w > 1)
                @as(f32, @floatFromInt(offset)) / @as(f32, @floatFromInt(bar_w - 1))
            else
                0.5;
            surface.writeCell(bc, row, .{
                .char = .{ .grapheme = "\xe2\x96\x88", .width = 1 },
                .style = .{ .fg = theme.lerpColor(theme.ACCENT_SOFT, theme.ACCENT, t), .bg = theme.PANEL },
            });
        }
        const ref_txt = try std.fmt.allocPrint(ctx.arena, "{d}", .{top_rule.refer_count});
        w.writeText(&surface, ctx, count_col, row, ref_txt, theme.fg(theme.MUTED));
        row += 1;
    }

    return surface;
}

pub fn handleModuleEvent(
    self: anytype,
    ctx: *vxfw.EventContext,
    key: vaxis.Key,
    rule_count: usize,
    member_count: usize,
) anyerror!void {
    if (key.matches(vaxis.Key.tab, .{})) {
        self.analysis.focus = switch (self.analysis.focus) {
            .rules => .members,
            else => .rules,
        };
        self.analysis.expanded_rule = null;
        self.analysis.show_member_detail = false;
        ctx.consumeAndRedraw();
        return;
    }
    if (key.matches('j', .{}) or key.matches(vaxis.Key.down, .{})) {
        switch (self.analysis.focus) {
            .rules => {
                if (rule_count > 0 and self.analysis.rule_cursor < rule_count - 1)
                    self.analysis.rule_cursor += 1;
                ctx.consumeAndRedraw();
                return;
            },
            .members => {
                if (member_count > 0 and self.analysis.member_cursor < member_count - 1)
                    self.analysis.member_cursor += 1;
                ctx.consumeAndRedraw();
                return;
            },
            else => {},
        }
    }
    if (key.matches('k', .{}) or key.matches(vaxis.Key.up, .{})) {
        switch (self.analysis.focus) {
            .rules => {
                self.analysis.rule_cursor -|= 1;
                ctx.consumeAndRedraw();
                return;
            },
            .members => {
                self.analysis.member_cursor -|= 1;
                ctx.consumeAndRedraw();
                return;
            },
            else => {},
        }
    }
    if (key.matches(vaxis.Key.enter, .{})) {
        switch (self.analysis.focus) {
            .rules => {
                if (self.analysis.expanded_rule == self.analysis.rule_cursor) {
                    self.analysis.expanded_rule = null;
                } else {
                    self.analysis.expanded_rule = self.analysis.rule_cursor;
                }
                ctx.consumeAndRedraw();
                return;
            },
            .members => {
                self.analysis.show_member_detail = !self.analysis.show_member_detail;
                ctx.consumeAndRedraw();
                return;
            },
            else => {},
        }
    }
    if (key.matches(vaxis.Key.escape, .{})) {
        if (self.analysis.expanded_rule != null) {
            self.analysis.expanded_rule = null;
        } else if (self.analysis.show_member_detail) {
            self.analysis.show_member_detail = false;
        } else {
            self.analysis.focus = .rules;
        }
        ctx.consumeAndRedraw();
    }
}

pub fn shortcuts(self: anytype) []const w.Shortcut {
    return switch (self.analysis.focus) {
        .rules => &.{
            .{ .key = "j/k", .label = "move" },
            .{ .key = "Enter", .label = "expand" },
            .{ .key = "Tab", .label = "switch focus" },
            .{ .key = "?", .label = "help" },
            .{ .key = "q", .label = "quit" },
        },
        .members => &.{
            .{ .key = "j/k", .label = "move" },
            .{ .key = "Enter", .label = "show detail" },
            .{ .key = "Tab", .label = "switch focus" },
            .{ .key = "?", .label = "help" },
            .{ .key = "q", .label = "quit" },
        },
        else => &.{
            .{ .key = "Tab", .label = "switch focus" },
            .{ .key = "?", .label = "help" },
            .{ .key = "q", .label = "quit" },
        },
    };
}

const RankMoveKind = enum {
    up,
    down,
    flat,
    none,
};

const RankMove = struct {
    text: []const u8,
    kind: RankMoveKind,
};

fn ruleRankMove(
    arena: std.mem.Allocator,
    rules: []const data.AnalysisRule,
    current_idx: usize,
) std.mem.Allocator.Error!RankMove {
    if (current_idx >= rules.len) return .{ .text = try arena.dupe(u8, "\xe2\x80\x94"), .kind = .none };

    const current = rules[current_idx];
    if (current.refer_count == 0) return .{ .text = try arena.dupe(u8, "\xe2\x80\x94"), .kind = .none };

    const current_previous_score = previousRankScore(current);
    if (current_previous_score == 0 and recentRankScore(current) > 0) {
        return .{ .text = try arena.dupe(u8, "new"), .kind = .up };
    }

    var previous_rank: usize = 1;
    for (rules, 0..) |other, other_idx| {
        if (other_idx == current_idx) continue;
        const other_previous_score = previousRankScore(other);
        if (other_previous_score > current_previous_score or
            (other_previous_score == current_previous_score and other_idx < current_idx))
        {
            previous_rank += 1;
        }
    }

    const current_rank = current_idx + 1;
    if (previous_rank > current_rank) {
        const delta = previous_rank - current_rank;
        return .{
            .text = try std.fmt.allocPrint(arena, "\xe2\x86\x91{d}", .{delta}),
            .kind = .up,
        };
    }
    if (previous_rank < current_rank) {
        const delta = current_rank - previous_rank;
        return .{
            .text = try std.fmt.allocPrint(arena, "\xe2\x86\x93{d}", .{delta}),
            .kind = .down,
        };
    }

    return .{ .text = try arena.dupe(u8, "\xe2\x80\x94"), .kind = .flat };
}

fn recentRankScore(rule: data.AnalysisRule) u32 {
    const window = 7;
    var recent: u32 = 0;
    for (rule.trend[rule.trend.len - window ..]) |value| recent += value;
    return recent;
}

fn previousRankScore(rule: data.AnalysisRule) u32 {
    const recent = recentRankScore(rule);
    return if (rule.refer_count > recent) rule.refer_count - recent else 0;
}

fn firstLineTrimmed(text: []const u8, max_cells: u16) []const u8 {
    var end: usize = text.len;
    if (std.mem.indexOfScalar(u8, text, '\n')) |newline| end = newline;
    var slice = text[0..end];
    const cap: usize = @intCast(max_cells);
    if (slice.len > cap) slice = slice[0..cap];
    return slice;
}
