-- amzxyz@https://github.com/amzxyz/rime-wanxiang
-- input_stats.lua
-- Rime 统计增强版 (LevelDB / 滚动时间窗口 / 效率仪表盘 / 汉字提纯)
-- 新增：配置项 YAML 外放、递进式时光机、自定义区间、UI横向自适应排版

local userdb = require("wanxiang/userdb")
local wanxiang = require("wanxiang/wanxiang")

local _db_pool = {}
local raw_software_name = rime_api.get_distribution_code_name()

local function get_db(env)
    local db_name = env.stats_db_name
    if not db_name or db_name == "" then
        db_name = env.engine.schema.config:get_string("input_stats/db_name")
        if not db_name or db_name == "" then db_name = "lua/stats" end
        env.stats_db_name = db_name
    end

    local db = _db_pool[db_name]
    if not db then
        db = userdb.LevelDb(db_name)
        _db_pool[db_name] = db
    end
    if db and not db:loaded() then db:open() end
    return db
end

local function process_platform_info(name, ver)
    name = name or ""
    ver = ver or ""
    ver = ver:match("^([vV]?%d+%.%d+%.%d+)") or ver
    if name == "Weasel" then name = "小狼毫" end
    if name == "trime" then name = "同文输入法" end
    if name == "hamster3" then name = "元书输入法" end
    if name == "hamster" then name = "仓输入法" end
    if name == "lyraime" then name = "灵韵输入法" end
    if name == "xime" then name = "曦码输入法" end
    if name == "Cobra​" then name = "元书输入法(PC)" end
    if name == "default" then name = "超越输入法" end
    return name, ver
end

local function is_chinese_code(c)
    return (c >= 0x4E00 and c <= 0x9FFF) or (c >= 0x3400 and c <= 0x4DBF) or 
           (c >= 0x20000 and c <= 0x2A6DF) or (c >= 0x2A700 and c <= 0x2B73F) or 
           (c >= 0x2B740 and c <= 0x2B81F) or (c >= 0x2B820 and c <= 0x2CEAF) or 
           (c >= 0x2CEB0 and c <= 0x2EBEF) or (c >= 0x30000 and c <= 0x3134F) or 
           (c >= 0x31350 and c <= 0x323AF) or (c >= 0x2EBF0 and c <= 0x2EE5F) or 
           (c >= 0xF900  and c <= 0xFAFF) or (c >= 0x2F800 and c <= 0x2FA1F) or 
           (c >= 0x2E80  and c <= 0x2EFF) or (c >= 0x2F00  and c <= 0x2FDF)
end

local function get_pure_chinese_length(text)
    local count = 0
    for _, code in utf8.codes(text) do
        if is_chinese_code(code) then count = count + 1 end
    end
    return count
end

local speed_buffer = {}
local last_cleanup_ts = 0
local pending_stats = {}      -- { [day_key] = { ["_len"]=N, ["_cnt"]=N, ... } }
local pending_max_speeds = {} -- { [day_key] = { ["_spd"]=N } }
local BATCH_INTERVAL = 5      -- 最多每5秒落盘一次
local MAX_PENDING_WORDS = 200 -- 积累超过200字时强制刷盘
local last_flush_ts = 0

local DEFAULT_TITLES = {
    { threshold = 5000000, name = "⌨️·天人合一" },
    { threshold = 1000000, name = "⌨️·登峰造极" },
    { threshold = 500000,  name = "✨·出神入化" },
    { threshold = 100000,  name = "💨·行云流水" },
    { threshold = 50000,   name = "🚀·运指如飞" },
    { threshold = 10000,   name = "🌟·渐入佳境" },
    { threshold = 0,       name = "🌱·初学乍练" }
}

local function get_current_kpm(now)
    if now - last_cleanup_ts > 5 then
        local new_buf = {}
        local threshold = now - 60
        for _, item in ipairs(speed_buffer) do
            if item.ts > threshold then table.insert(new_buf, item) end
        end
        speed_buffer = new_buf
        last_cleanup_ts = now
    end
    local total = 0
    local threshold = now - 60
    for _, item in ipairs(speed_buffer) do
        if item.ts > threshold then total = total + item.len end
    end
    return total
end

local function pending_accum(day_key, suffix, amount)
    if not pending_stats[day_key] then pending_stats[day_key] = {} end
    pending_stats[day_key][suffix] = (pending_stats[day_key][suffix] or 0) + amount
end

local function pending_max(day_key, suffix, new_val)
    if not pending_max_speeds[day_key] then pending_max_speeds[day_key] = {} end
    local old = pending_max_speeds[day_key][suffix] or 0
    if new_val > old then pending_max_speeds[day_key][suffix] = new_val end
end

local function do_flush(env)
    if not pending_stats or next(pending_stats) == nil then return end
    local db = get_db(env)
    if not db or not db:loaded() then return end
    
    for day_key, fields in pairs(pending_stats) do
        for suffix, amount in pairs(fields) do
            local d_key = day_key .. suffix
            local old_val = tonumber(db:fetch(d_key)) or 0
            db:update(d_key, tostring(old_val + amount))
            local t_key = "total" .. suffix
            local total_val = tonumber(db:fetch(t_key)) or 0
            db:update(t_key, tostring(total_val + amount))
        end
    end
    
    for day_key, max_fields in pairs(pending_max_speeds) do
        for suffix, new_val in pairs(max_fields) do
            local d_key = day_key .. suffix
            local old_val = tonumber(db:fetch(d_key)) or 0
            if new_val > old_val then db:update(d_key, tostring(new_val)) end
            local t_key = "total" .. suffix
            local total_val = tonumber(db:fetch(t_key)) or 0
            if new_val > total_val then db:update(t_key, tostring(new_val)) end
        end
    end
    
    pending_stats = {}
    pending_max_speeds = {}
    last_flush_ts = os.time()
end

local function try_flush(env)
    local now = os.time()
    -- 估算 pending 总字数
    local total_words = 0
    for _, fields in pairs(pending_stats) do
        total_words = total_words + (fields["_len"] or 0)
    end
    if total_words >= MAX_PENDING_WORDS or now - last_flush_ts >= BATCH_INTERVAL then
        do_flush(env)
    end
end

local function record_stats_mem(env, hanzi_len, code_len)
    local now = os.time()
    local t = os.date("*t", now)
    local day_key = string.format("d_%04d%02d%02d", t.year, t.month, t.day)
    
    local current_kpm = 0
    if hanzi_len <= 30 then table.insert(speed_buffer, {ts = now, len = hanzi_len}) end
    current_kpm = get_current_kpm(now)
    
    pending_accum(day_key, "_len", hanzi_len)
    pending_accum(day_key, "_cnt", 1)
    pending_accum(day_key, "_code", code_len)
    
    if hanzi_len == 1 then pending_accum(day_key, "_l1", 1)
    elseif hanzi_len == 2 then pending_accum(day_key, "_l2", 1)
    elseif hanzi_len == 3 then pending_accum(day_key, "_l3", 1)
    elseif hanzi_len == 4 then pending_accum(day_key, "_l4", 1)
    elseif hanzi_len > 4 then pending_accum(day_key, "_l_gt4", 1) end
    
    pending_max(day_key, "_spd", current_kpm)
end

local function db_get(db, key)
    return tonumber(db:fetch(key)) or 0
end

local function clear_all_data(env)
    local db = get_db(env)
    if not db or not db:loaded() then return false end
    
    if db.empty then
        db:empty()
        speed_buffer = {}
        pending_stats = {}
        pending_max_speeds = {}
        last_flush_ts = os.time()
        return true
    end
    local iter = db:query("")
    if iter then
        local keys = {}
        for key, _ in iter do table.insert(keys, key) end
        for _, key in ipairs(keys) do db:erase(key) end
        speed_buffer = {}
        pending_stats = {}
        pending_max_speeds = {}
        last_flush_ts = os.time()
        return true
    end
    return false
end

local function aggregate_stats(env, days_lookback)
    local db = get_db(env)
    if not db or not db:loaded() then return nil end
    
    if days_lookback == 0 then
        local prefix = "total"
        return {
            len = db_get(db, prefix .. "_len"), cnt = db_get(db, prefix .. "_cnt"), code = db_get(db, prefix .. "_code"),
            spd = db_get(db, prefix .. "_spd"), l1 = db_get(db, prefix .. "_l1"), l2 = db_get(db, prefix .. "_l2"),
            l3 = db_get(db, prefix .. "_l3"), l4 = db_get(db, prefix .. "_l4"), l_gt4 = db_get(db, prefix .. "_l_gt4")
        }
    end

    local res = {len=0, cnt=0, code=0, spd=0, l1=0, l2=0, l3=0, l4=0, l_gt4=0}
    local now_ts = os.time()
    
    for i = 0, days_lookback - 1 do
        local target_ts = now_ts - (i * 86400)
        local t = os.date("*t", target_ts)
        local day_key = string.format("d_%04d%02d%02d", t.year, t.month, t.day)
        
        res.len = res.len + db_get(db, day_key .. "_len")
        res.cnt = res.cnt + db_get(db, day_key .. "_cnt")
        res.code = res.code + db_get(db, day_key .. "_code")
        res.l1 = res.l1 + db_get(db, day_key .. "_l1")
        res.l2 = res.l2 + db_get(db, day_key .. "_l2")
        res.l3 = res.l3 + db_get(db, day_key .. "_l3")
        res.l4 = res.l4 + db_get(db, day_key .. "_l4")
        res.l_gt4 = res.l_gt4 + db_get(db, day_key .. "_l_gt4")
        
        local daily_spd = db_get(db, day_key .. "_spd")
        if daily_spd > res.spd then res.spd = daily_spd end
    end
    return res
end

local function aggregate_custom_period(env, year, month, day, end_year, end_month, end_day)
    local db = get_db(env)
    if not db or not db:loaded() then return nil end
    local keys = {}

    if end_year then
        local start_ts = os.time({year=year, month=month, day=day, hour=12})
        local end_ts = os.time({year=end_year, month=end_month, day=end_day, hour=12})
        if start_ts and end_ts and start_ts <= end_ts then
            local current_ts = start_ts
            while current_ts <= end_ts do
                local t = os.date("*t", current_ts)
                table.insert(keys, string.format("d_%04d%02d%02d", t.year, t.month, t.day))
                current_ts = current_ts + 86400
            end
        end
    elseif day then
        table.insert(keys, string.format("d_%04d%02d%02d", year, month, day))
    elseif month then
        for d = 1, 31 do table.insert(keys, string.format("d_%04d%02d%02d", year, month, d)) end
    elseif year then
        for m = 1, 12 do
            for d = 1, 31 do table.insert(keys, string.format("d_%04d%02d%02d", year, m, d)) end
        end
    end

    if #keys == 0 then return nil end

    local res = {len=0, cnt=0, code=0, spd=0, l1=0, l2=0, l3=0, l4=0, l_gt4=0}
    local has_data = false

    for _, day_key in ipairs(keys) do
        local len = db_get(db, day_key .. "_len")
        if len > 0 then
            has_data = true
            res.len = res.len + len
            res.cnt = res.cnt + db_get(db, day_key .. "_cnt")
            res.code = res.code + db_get(db, day_key .. "_code")
            res.l1 = res.l1 + db_get(db, day_key .. "_l1")
            res.l2 = res.l2 + db_get(db, day_key .. "_l2")
            res.l3 = res.l3 + db_get(db, day_key .. "_l3")
            res.l4 = res.l4 + db_get(db, day_key .. "_l4")
            res.l_gt4 = res.l_gt4 + db_get(db, day_key .. "_l_gt4")

            local daily_spd = db_get(db, day_key .. "_spd")
            if daily_spd > res.spd then res.spd = daily_spd end
        end
    end

    if not has_data then return nil end
    return res
end

local function ensure_titles(env)
    if env.titles then return env.titles end

    local titles = {}
    local custom_titles = env.engine.schema.config:get_list("input_stats/titles")
    if custom_titles then
        for i = 0, custom_titles.size - 1 do
            local item = custom_titles:get_value_at(i)
            local value = item and item.value
            if value then
                local threshold, name = value:match("^(%d+):(.+)$")
                if threshold and name then
                    titles[#titles + 1] = { threshold = tonumber(threshold), name = name }
                end
            end
        end
    end

    if #titles == 0 then
        env.titles = DEFAULT_TITLES
    else
        table.sort(titles, function(a, b) return a.threshold > b.threshold end)
        env.titles = titles
    end
    return env.titles
end

local function get_user_title(env)
    local db = get_db(env)
    if not db or not db:loaded() then return "初学乍练" end

    local current_len = db_get(db, "total_len")
    for _, item in ipairs(ensure_titles(env)) do
        if current_len >= item.threshold then return item.name end
    end
    return "初学乍练"
end

local function draw_bar(percent)
    local length = 10
    local filled_len = math.floor((percent / 100) * length)
    local empty_len = length - filled_len
    return string.rep("▓", filled_len) .. string.rep("░", empty_len)
end

local function format_summary(title, subtitle, data, env)
    if not data or data.cnt == 0 then return "※ " .. title .. "暂无数据" end
    
    local avg_code = 0
    if data.len > 0 then avg_code = data.code / data.len end
    
    local phrase_rate = 0
    if data.len > 0 then phrase_rate = (data.len - data.l1) / data.len * 100 end

    local estimated_avg_spd = 0
    if data.cnt > 0 then
        estimated_avg_spd = math.floor(data.len / ((data.cnt * 2) / 60))
        if estimated_avg_spd > data.spd then estimated_avg_spd = math.floor(data.spd * 0.8) end
        if estimated_avg_spd == 0 and data.len > 0 then estimated_avg_spd = data.len end
    end

    local p1 = (data.l1 / data.cnt) * 100
    local p2 = (data.l2 / data.cnt) * 100
    local p3 = (data.l3 / data.cnt) * 100
    local p4 = (data.l4 / data.cnt) * 100
    local p_gt4 = (data.l_gt4 / data.cnt) * 100
    
    local raw_ver = rime_api.get_distribution_version() or ""
    local clean_name, clean_ver = process_platform_info(raw_software_name, raw_ver)
    local user_achievement = get_user_title(env)
    local finger_style = wanxiang.get_input_method_type(env)
    local finger_style_map = { ["pinyin"]="全拼", ["zrm"]="自然码", ["flypy"]="小鹤双拼", ["mspy"]="微软双拼", ["sogou"]="搜狗双拼", ["abc"]="智能ABC", ["ziguang"]="紫光双拼", ["pyjj"]="拼音加加", ["gbpy"]="国标双拼", ["zrlong"]="自然龙", ["hxlong"]="汉心龙", ["ltsp"]="蓝天双拼", ["lxsq"]="乱序17", ["sdpy"]="首道双拼", ["t9"]="九键" }
    local finger_label = finger_style_map[finger_style] or finger_style
    local header = string.format("※ %s统计 · 效率仪表盘\n", title)
    if subtitle and subtitle ~= "" then
        header = header .. string.format("📅 %s\n", subtitle)
    end
    local zwsp = "\226\128\139"
    return header .. string.format(
        "───────────────" .. zwsp .. "\n" ..
        "📊 综合数据" .. zwsp .. "\n" ..
        "  均速:%-5d 上屏:%d" .. zwsp .. "\n" ..
        "  峰速:%-5d 字数:%d" .. zwsp .. "\n" ..
        "🏆 段位：%s" .. zwsp .. "\n" ..
        "───────────────" .. zwsp .. "\n" ..
        "⚡ 核心效率" .. zwsp .. "\n" ..
        "  平均编码：%.2f 键/字" .. zwsp .. "\n" ..
        "  词组连打：%.1f %%" .. zwsp .. "\n" ..
        "───────────────" .. zwsp .. "\n" ..
        "📈 字词分布" .. zwsp .. "\n" ..
        "  [1] %3d%% %s" .. zwsp .. "\n" ..
        "  [2] %3d%% %s" .. zwsp .. "\n" ..
        "  [3] %3d%% %s" .. zwsp .. "\n" ..
        "  [4] %3d%% %s" .. zwsp .. "\n" ..
        "  [+] %2d%% %s" .. zwsp .. "\n" ..
        "───────────────" .. zwsp .. "\n" ..
        "◉ 方案：%s" .. zwsp .. "\n" ..
        "◉ 编码：%s" .. zwsp .. "\n" ..
        "◉ 前端：%s %s" .. zwsp,
        math.floor(estimated_avg_spd), math.floor(data.cnt),
        math.floor(data.spd), math.floor(data.len),
        user_achievement,
        avg_code, phrase_rate,
        math.floor(p1), draw_bar(p1), 
        math.floor(p2), draw_bar(p2), 
        math.floor(p3), draw_bar(p3), 
        math.floor(p4), draw_bar(p4), 
        math.floor(p_gt4), draw_bar(p_gt4),
        env.schema_name, finger_label, clean_name, clean_ver
    )
end

local function yield_msg(seg, text, icon)
    yield(Candidate("stat", seg.start, seg._end, text, icon or "🕰️"))
end

local function init(env)
    local config = env.engine.schema.config
    env.schema_name = env.engine.schema.schema_name or "万象方案"
    env.stats_db_name = config:get_string("input_stats/db_name")
    if not env.stats_db_name or env.stats_db_name == "" then env.stats_db_name = "lua/stats" end
    env.titles = nil

    -- 避免首次提交因为 last_flush_ts=0 立刻打开数据库。
    if last_flush_ts == 0 then last_flush_ts = os.time() end

    env.triggers = {
        clear   = config:get_string("input_stats/triggers/clear")   or "/qctj",
        today   = config:get_string("input_stats/triggers/today")   or "/rtj",
        week    = config:get_string("input_stats/triggers/week")    or "/ztj",
        month   = config:get_string("input_stats/triggers/month")   or "/ytj",
        year    = config:get_string("input_stats/triggers/year")    or "/ntj",
        total   = config:get_string("input_stats/triggers/total")   or "/tj",
        history = config:get_string("input_stats/triggers/history") or "/htj",
    }

    if env.stat_notifier then env.stat_notifier:disconnect() end
    local ctx = env.engine.context
    
    env.stat_notifier = ctx.commit_notifier:connect(function(ctx)
        local commit_text = ctx:get_commit_text()
        if not commit_text or commit_text == "" then return end
        if commit_text:sub(1, 1) == "/" then return end
        if commit_text:find("^[※◉🏆📊⚡📈]") then return end

        local hanzi_len = get_pure_chinese_length(commit_text)
        if hanzi_len == 0 then return end
        local raw_input = ctx.input or ""
        local code_len = string.len(raw_input)
        if code_len == 0 then code_len = hanzi_len * 2 end 

        record_stats_mem(env, hanzi_len, code_len)
        try_flush(env)
    end)
end

local function fini(env)
    do_flush(env)
    if env.stat_notifier then 
        env.stat_notifier:disconnect() 
        env.stat_notifier = nil
    end
end

local function translator(input, seg, env)
    try_flush(env)
    local summary = ""
    local data = nil
    local title = ""
    local subtitle = ""

    if input == env.triggers.clear then
        if clear_all_data(env) then yield_msg(seg, "※ 统计数据已全部清空。", "🗑️")
        else yield_msg(seg, "※ 数据清空失败，请检查权限。", "❌") end
        return
    end

    if input == env.triggers.today then title = "今日"; data = aggregate_stats(env, 1)
    elseif input == env.triggers.week then title = "七日"; data = aggregate_stats(env, 7)
    elseif input == env.triggers.month then title = "卅日"; data = aggregate_stats(env, 30)
    elseif input == env.triggers.year then title = "本年"; data = aggregate_stats(env, 365)
    elseif input == env.triggers.total then title = "生涯"; data = aggregate_stats(env, 0)
    end

    if not data then
        local trigger_len = string.len(env.triggers.history)
        
        if string.sub(input, 1, trigger_len) == env.triggers.history then
            local safe_trigger = env.triggers.history:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
            local is_matched = false
            
            local s_y, s_m, s_d, e_y, e_m, e_d = input:match("^" .. safe_trigger .. "(%d%d%d%d)(%d%d)(%d%d)t(%d%d%d%d)(%d%d)(%d%d)$")
            if s_y then
                is_matched = true
                title = "区间"
                subtitle = string.format("%s.%s.%s - %s.%s.%s", s_y, s_m, s_d, e_y, e_m, e_d)
                data = aggregate_custom_period(env, tonumber(s_y), tonumber(s_m), tonumber(s_d), tonumber(e_y), tonumber(e_m), tonumber(e_d))
                if not data then return yield_msg(seg, "※ 该区间内没有留下打字记录哦") end
            else
                local y, m, d = input:match("^" .. safe_trigger .. "(%d%d%d%d)(%d%d)(%d%d)$")
                if y then
                    is_matched = true
                    title = "单日"
                    subtitle = string.format("%s.%s.%s", y, m, d)
                    data = aggregate_custom_period(env, tonumber(y), tonumber(m), tonumber(d))
                    if not data then return yield_msg(seg, "※ 这一天没有留下打字记录哦") end
                else
                    local y, m = input:match("^" .. safe_trigger .. "(%d%d%d%d)(%d%d)$")
                    if y then
                        is_matched = true
                        title = "月份"
                        subtitle = string.format("%s年%s月", y, m)
                        data = aggregate_custom_period(env, tonumber(y), tonumber(m))
                        if not data then return yield_msg(seg, "※ 该月没有留下打字记录哦") end
                    else
                        local y = input:match("^" .. safe_trigger .. "(%d%d%d%d)$")
                        if y then
                            is_matched = true
                            title = "年度"
                            subtitle = string.format("%s年", y)
                            data = aggregate_custom_period(env, tonumber(y))
                            if not data then return yield_msg(seg, "※ 该年没有留下打字记录哦") end
                        end
                    end
                end
            end
            
            if not is_matched then
                if string.len(input) == trigger_len then
                    return yield_msg(seg, "※ 请输入日期或区间 (例: 2026, 202601, 20260101t20260201)", "⌨️")
                else
                    local query_str = string.sub(input, trigger_len + 1)
                    if string.find(query_str, "t") then
                        return yield_msg(seg, "※ 正在输入区间查询...", "⏳")
                    else
                        return yield_msg(seg, "※ 正在查询中... 请继续输入完整的年/月/日", "⏳")
                    end
                end
            end
        end
    end

    if data then
        summary = format_summary(title, subtitle, data, env)
        yield(Candidate("stat", seg.start, seg._end, summary, "📊"))
    end
end

return { init = init, func = translator, fini = fini }