#include "fgc/ui/TuiUi.h"

#include "fgc/DumpParser.h"
#include "fgc/HelpText.h"
#include "fgc/Logger.h"

#include <chrono>
#include <cstdio>
#include <map>
#include <sstream>
#include <vector>

#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
#include <ftxui/screen/color.hpp>

namespace fgc {

using namespace ftxui;

namespace {

Color toColor(UiColor c) {
    switch (c) {
        case UiColor::Green:   return Color::Green;
        case UiColor::Yellow:  return Color::Yellow;
        case UiColor::Red:     return Color::Red;
        case UiColor::Cyan:    return Color::Cyan;
        case UiColor::Dim:     return Color::GrayDark;
        case UiColor::Default:
        default:               return Color::Default;
    }
}

// A bordered panel with a colored, bold title.
Element panel(const std::string& title, Color title_color, Element body) {
    return window(text(" " + title + " ") | bold | color(title_color), std::move(body)) | flex;
}

// "key" + label pair for the nano-style bottom bar.
Element keyHint(const std::string& key, const std::string& label) {
    return hbox({text(" " + key + " ") | inverted, text(" " + label + "  ")});
}

Element badge(const std::string& s, bool on, Color on_color) {
    auto e = text(" " + s + " ");
    return on ? (e | color(on_color) | bold) : (e | dim);
}

Element axisRow(const AxisView& a) {
    Element state = text(std::string(" ") + axisStateLabel(a.state) + " ")
                    | color(toColor(axisStateColor(a.state))) | bold;
    Element heading = text(formatDegrees(a.deg)) | bold;
    return vbox({
        hbox({text(a.label + "  ") | bold, state, filler(),
              text("→ " + formatDegrees(a.target_deg)) | dim}),
        hbox({text("  "), heading,
              text("   x=" + std::to_string(a.xactual)) | dim,
              text("  enc=" + std::to_string(a.xenc)) | dim}),
        hbox({text("  "),
              badge("STILL", a.standstill, Color::GrayLight),
              badge("MOVE", a.moving, Color::Cyan),
              badge("STALL", a.stall, Color::Red),
              badge("OT", a.overtemp, Color::Red),
              badge("L", a.endstop_l, Color::Yellow),
              badge("R", a.endstop_r, Color::Yellow)}),
    });
}

Element gimbalPanel(const GimbalView& g) {
    std::vector<Element> rows;
    if (!g.present) rows.push_back(text("link down") | color(Color::Red) | bold);
    rows.push_back(axisRow(g.yaw));
    if (g.pitch_present) {
        rows.push_back(separator());
        rows.push_back(axisRow(g.pitch));
    }
    return panel("GIMBAL", Color::Cyan, vbox(std::move(rows)));
}

Element sensorsPanel(const SensorsView& s) {
    std::vector<Element> rows;
    rows.push_back(text("pending integration") | dim);
    rows.push_back(separator());
    for (const auto& f : s.fields) {
        Element val = text(f.value + (f.unit.empty() ? "" : " " + f.unit));
        val = f.present ? (val | bold) : (val | dim);
        rows.push_back(hbox({text(f.label) | dim, filler(), val}));
    }
    return panel("SENSORS  (DHT11 · MTi)", Color::Magenta, vbox(std::move(rows)));
}

Element cameraPanel(const CaptureView& c) {
    std::string labels;
    for (size_t i = 0; i < c.labels.size(); ++i)
        labels += (i ? "," : "") + c.labels[i];

    Element active = c.active ? (text(" CAPTURING ") | color(Color::Green) | bold)
                              : (text(" idle ") | dim);
    std::vector<Element> rows = {
        hbox({text("cameras  ") | dim, text(std::to_string(c.camera_count) + "  "),
              text(labels) | dim}),
        hbox({text("capture  ") | dim, active, filler(),
              text(std::to_string(c.image_rate) + " img/s") | dim}),
    };
    if (c.has_last) {
        long long now = std::chrono::duration_cast<std::chrono::milliseconds>(
                            std::chrono::system_clock::now().time_since_epoch()).count();
        rows.push_back(hbox({text("last     ") | dim,
                             text(c.last_label + "  "),
                             text(formatDegrees(c.last_heading_deg) + " / " +
                                  formatDegrees(c.last_pitch_deg) + "  ") | dim,
                             text(formatTimeAgo(now, c.last_ts_ms)) | dim}));
    } else {
        rows.push_back(hbox({text("last     ") | dim, text("—") | dim}));
    }
    return panel("CAMERA", Color::Blue, vbox(std::move(rows)));
}

Element connPanel(const ConnView& v) {
    Element mqtt = !v.mqtt_enabled ? (text(" disabled ") | dim)
                 : v.mqtt_connected ? (text(" connected ") | color(Color::Green) | bold)
                                    : (text(" disconnected ") | color(Color::Red) | bold);
    std::string mode = v.control_code == 0 ? "0 auto-sweep" : "1 directed";
    return panel("CONNECTIVITY", Color::Green, vbox({
        hbox({text("MQTT     ") | dim, mqtt, filler(), text(v.broker) | dim}),
        hbox({text("tower    ") | dim, text(v.tower)}),
        hbox({text("control  ") | dim, text(mode),
              filler(), text("hdg " + v.target_heading) | dim}),
        hbox({text("status   ") | dim, text(std::to_string(v.last_status_code))}),
    }));
}

Element logPanel(const std::vector<LogLine>& lines) {
    std::vector<Element> rows;
    for (const auto& l : lines) {
        Color c = Color::Default;
        switch (l.level) {
            case LogLevel::Error: c = Color::Red; break;
            case LogLevel::Warn:  c = Color::Yellow; break;
            case LogLevel::Debug:
            case LogLevel::Trace: c = Color::GrayDark; break;
            default: break;
        }
        rows.push_back(text(l.text) | color(c));
    }
    if (rows.empty()) rows.push_back(text("(no log output yet)") | dim);
    return window(text(" LOG ") | bold | color(Color::GrayLight),
                  vbox(std::move(rows)) | focusPositionRelative(0, 1) | yframe);
}

// Inline help pane (toggled with '?'). Lists every command section; the
// `sel`-th section is expanded to show each entry's detail. The Diagnostics
// section additionally renders the last captured firmware DUMP block.
Element helpPanel(int sel, const DumpView& dump) {
    const auto& cat = helpCatalog();
    std::vector<Element> rows;
    for (int i = 0; i < static_cast<int>(cat.size()); ++i) {
        const HelpSection& sec = cat[i];
        const bool open = (i == sel);
        Element title = hbox({
            text(open ? " v " : " > "),
            text(sec.title) | bold,
            text("  " + sec.blurb) | dim,
        });
        rows.push_back(open ? (title | inverted) : title);
        if (!open) continue;

        for (const auto& e : sec.entries) {
            rows.push_back(hbox({text("    "), text(e.syntax) | color(Color::Cyan) | bold}));
            rows.push_back(hbox({text("      "), text(e.summary) | dim}));
            for (const auto& d : e.detail)
                rows.push_back(hbox({text("        "), text(d) | dim}));
        }
        // Diagnostics: show the most recent firmware dump inline.
        if (sec.title == "Diagnostics") {
            rows.push_back(text("    --- last firmware dump ---") | bold);
            if (dump.has) {
                std::istringstream iss(dump.text);
                std::string l;
                while (std::getline(iss, l))
                    rows.push_back(hbox({text("      "), text(l) | color(Color::Green)}));
            } else {
                rows.push_back(hbox({text("      "),
                                     text("(none captured yet - run 'dump')") | dim}));
            }
        }
        rows.push_back(text(""));
    }
    return window(text(" HELP  (?:close  Up/Down:section) ") | bold | color(Color::Cyan),
                  vbox(std::move(rows)) | yframe);
}

// Aligned "label: value" row for the detail view.
Element kvRow(const std::string& k, const std::string& v, Color vc = Color::Default) {
    return hbox({text(k) | dim | size(WIDTH, EQUAL, 12), text(v) | color(vc)});
}

std::string hex8(unsigned v) {
    char buf[11];
    std::snprintf(buf, sizeof(buf), "0x%08X", v);
    return buf;
}

std::string get_or(const std::map<std::string, std::string>& m, const std::string& k) {
    auto it = m.find(k);
    return it == m.end() ? "?" : it->second;
}

// Live per-axis block: everything we get from the ST telemetry line.
Element axisLiveDetail(const AxisView& a) {
    Element state = text(std::string(" ") + axisStateLabel(a.state) + " ")
                    | color(toColor(axisStateColor(a.state))) | bold;
    std::vector<Element> rows = {
        hbox({text(a.label + "  ") | bold, state, filler(),
              text(formatDegrees(a.deg) + "  ->  " + formatDegrees(a.target_deg)) | bold}),
        kvRow("xactual", std::to_string(a.xactual)),
        kvRow("xenc", std::to_string(a.xenc)),
        kvRow("SG_RESULT", std::to_string(a.sg)),
        kvRow("CS_ACTUAL", std::to_string(a.cs)),
        kvRow("PWM", std::to_string(a.pwm)),
        kvRow("DRV_STATUS", hex8(a.drv_status), Color::Cyan),
        hbox({text("flags") | dim | size(WIDTH, EQUAL, 12),
              badge("STILL", a.standstill, Color::GrayLight),
              badge("MOVE", a.moving, Color::Cyan),
              badge("STALL", a.stall, Color::Red),
              badge("OT", a.overtemp, Color::Red),
              badge("L", a.endstop_l, Color::Yellow),
              badge("R", a.endstop_r, Color::Yellow)}),
    };
    return vbox(std::move(rows));
}

// Decoded register block for one axis from a parsed firmware dump.
Element axisDumpDetail(const DumpAxis& ax) {
    auto reg = [&](const char* k) -> Element {
        auto it = ax.regs.find(k);
        return kvRow(k, it == ax.regs.end() ? "-" : it->second);
    };
    auto flags = [](const std::vector<std::string>& f) {
        std::string s;
        for (size_t i = 0; i < f.size(); ++i) s += (i ? " " : "") + f[i];
        return s;
    };
    auto statusRow = [&](const char* name, const std::string& raw,
                         const std::vector<std::string>& f) {
        return hbox({text(name) | dim | size(WIDTH, EQUAL, 12),
                     text(raw + "  ") | color(Color::Cyan),
                     text("[" + flags(f) + "]") | dim});
    };
    return vbox({
        hbox({text(std::string(1, ax.axis) + "  ") | bold,
              text(ax.state_name) | color(Color::Green) | bold,
              text("  enabled=" + std::to_string(ax.enabled ? 1 : 0)) | dim,
              text("  enc=" + std::to_string(ax.has_encoder ? 1 : 0)) | dim,
              text("  eeprom=" + std::to_string(ax.eeprom_restored ? 1 : 0)) | dim}),
        kvRow("limits", "[" + std::to_string(ax.lim_neg) + ".." + std::to_string(ax.lim_pos) + "]"),
        kvRow("hold_tgt", std::to_string(ax.hold_target)),
        reg("GCONF"), reg("CHOPCONF"), reg("XACTUAL"), reg("X_ENC"),
        reg("XTARGET"), reg("VACTUAL"),
        statusRow("DRV_STATUS", get_or(ax.regs, "DRV_STATUS"), ax.drv_flags),
        hbox({text("") | size(WIDTH, EQUAL, 12),
              text("CS_ACTUAL=" + std::to_string(ax.cs_actual) +
                   "  SG_RESULT=" + std::to_string(ax.sg_result)) | dim}),
        statusRow("GSTAT", get_or(ax.regs, "GSTAT"), ax.gstat_flags),
        statusRow("RAMP_STAT", get_or(ax.regs, "RAMP_STAT"), ax.ramp_flags),
    });
}

// Full-screen gimbal view (the takeover panel toggled with 'g'): live telemetry
// for both axes on the left, the decoded firmware register dump on the right.
Element gimbalDetailPanel(const GimbalView& g, const DumpView& dump) {
    // Live column.
    std::vector<Element> live;
    if (!g.present) live.push_back(text("link down") | color(Color::Red) | bold);
    live.push_back(axisLiveDetail(g.yaw));
    if (g.pitch_present) {
        live.push_back(separator());
        live.push_back(axisLiveDetail(g.pitch));
    }
    Element live_col = panel("GIMBAL · live", Color::Cyan, vbox(std::move(live)));

    // Register column (decoded firmware dump).
    std::vector<Element> regs;
    DumpData d = parseDump(dump.text);
    if (!d.valid) {
        regs.push_back(text("(requesting firmware dump...)") | dim);
        regs.push_back(text("press 'd' to refresh") | dim);
    } else {
        regs.push_back(hbox({text("build " + d.build) | dim, filler(),
                             text("up " + std::to_string(d.uptime_ms) + "ms") | dim}));
        {
            std::string rc;
            for (size_t i = 0; i < d.reset_flags.size(); ++i)
                rc += (i ? " " : "") + d.reset_flags[i];
            regs.push_back(text("reset: " + rc) | dim);
        }
        for (const auto& ax : d.axes) {
            regs.push_back(separator());
            regs.push_back(axisDumpDetail(ax));
        }
    }
    Element reg_col = panel("GIMBAL · firmware registers", Color::Cyan,
                            vbox(std::move(regs)) | yframe);

    return window(text(" GIMBAL  (g/Esc:close  d:refresh dump) ") | bold | color(Color::Cyan),
                  hbox({live_col, reg_col}));
}

}  // namespace

TuiUi::TuiUi() = default;

TuiUi::~TuiUi() { stop(); }

void TuiUi::start(SnapshotFn snapshot, CommandSink sink) {
    snapshot_ = std::move(snapshot);
    sink_     = std::move(sink);
    running_  = true;

    // Divert log output into the on-screen pane so it never corrupts the screen.
    Logger::setSink([this](LogLevel lvl, const std::string& line) { pushLog(lvl, line); });

    ui_thread_      = std::thread(&TuiUi::uiLoop, this);
    refresh_thread_ = std::thread(&TuiUi::refreshLoop, this);
}

void TuiUi::stop() {
    if (!running_.exchange(false)) return;
    if (auto* s = screen_.load()) s->Exit();  // breaks the FTXUI Loop
    if (ui_thread_.joinable()) ui_thread_.join();
    if (refresh_thread_.joinable()) refresh_thread_.join();
    Logger::setSink({});  // restore the default stdout/stderr writer
}

void TuiUi::pushLog(LogLevel level, const std::string& line) {
    std::lock_guard<std::mutex> lock(log_mutex_);
    log_.push_back({level, line});
    while (log_.size() > kLogCap) log_.pop_front();
}

void TuiUi::refreshLoop() {
    using namespace std::chrono_literals;
    while (running_) {
        std::this_thread::sleep_for(100ms);
        if (!running_) break;
        if (auto* s = screen_.load()) s->PostEvent(Event::Custom);  // force a redraw
    }
}

void TuiUi::uiLoop() {
    std::string cmd_buffer;
    bool        command_mode = false;
    enum class Overlay { None, Help, Gimbal };
    Overlay     overlay      = Overlay::None;  // which takeover panel owns the main area
    int         help_sel     = 0;
    bool        gimbal_dump_requested = false;  // auto-pull a dump the first time

    auto input = Input(&cmd_buffer, "type a command, Enter to run, Esc to cancel");

    auto renderer = Renderer(input, [&] {
        UiSnapshot s = snapshot_ ? snapshot_() : UiSnapshot{};
        {
            std::lock_guard<std::mutex> lock(log_mutex_);
            s.log.assign(log_.begin(), log_.end());
        }

        std::string mode = s.header.live ? "LIVE" : "MOCK";
        Element header = hbox({
            text(" FIREWATCH TOWER ") | bold | inverted,
            text("  " + s.header.tower + "  ") | bold,
            text("build " + s.header.build) | dim,
            filler(),
            text(" " + mode + " ") | (s.header.live ? color(Color::Green) : color(Color::Yellow)) | bold,
            text("  up " + std::to_string(s.header.uptime_ms / 1000) + "s ") | dim,
        });

        Element top    = hbox({gimbalPanel(s.gimbal), sensorsPanel(s.sensors)});
        Element middle = hbox({cameraPanel(s.capture), connPanel(s.conn)});

        Element bottom;
        if (command_mode) {
            bottom = hbox({text(" : ") | inverted, input->Render() | flex}) | border;
        } else {
            bottom = hbox({
                keyHint("s", "Start"), keyHint("x", "Stop"), keyHint("h", "Home"),
                keyHint("r", "Reset"), keyHint("g", "Gimbal"), keyHint(":", "Cmd"),
                keyHint("?", "Help"), filler(), keyHint("q", "Quit"),
            });
        }

        Element main;
        switch (overlay) {
            case Overlay::Help:   main = helpPanel(help_sel, s.dump); break;
            case Overlay::Gimbal: main = gimbalDetailPanel(s.gimbal, s.dump); break;
            default:              main = logPanel(s.log); break;
        }
        return vbox({header, separator(), top, middle, main | flex, bottom});
    });

    auto root = CatchEvent(renderer, [&](Event e) {
        if (command_mode) {
            if (e == Event::Return) {
                if (!cmd_buffer.empty() && sink_) sink_(cmd_buffer);
                cmd_buffer.clear();
                command_mode = false;
                return true;
            }
            if (e == Event::Escape) {
                cmd_buffer.clear();
                command_mode = false;
                return true;
            }
            return false;  // let the Input edit the buffer
        }
        const int n = static_cast<int>(helpCatalog().size());
        if (overlay == Overlay::Help) {  // help pane navigation
            if (e == Event::ArrowDown) { help_sel = (help_sel + 1) % n; return true; }
            if (e == Event::ArrowUp)   { help_sel = (help_sel - 1 + n) % n; return true; }
        }
        if (overlay != Overlay::None && e == Event::Escape) { overlay = Overlay::None; return true; }
        if (!e.is_character()) return false;
        const std::string& c = e.character();
        if (c == "?") {
            overlay = (overlay == Overlay::Help) ? Overlay::None : Overlay::Help;
            return true;
        }
        if (c == "g") {
            if (overlay == Overlay::Gimbal) {
                overlay = Overlay::None;
            } else {
                overlay = Overlay::Gimbal;
                if (!gimbal_dump_requested) {  // auto-pull a dump the first time
                    if (sink_) sink_("dump");
                    gimbal_dump_requested = true;
                }
            }
            return true;
        }
        if (overlay == Overlay::Gimbal && c == "d") {  // manual dump refresh
            if (sink_) sink_("dump");
            return true;
        }
        if (overlay == Overlay::Help) {  // vim-style section nav while help is open
            if (c == "j") { help_sel = (help_sel + 1) % n; return true; }
            if (c == "k") { help_sel = (help_sel - 1 + n) % n; return true; }
        }
        if (c == "q") { if (sink_) sink_("exit"); return true; }
        if (c == "s") { if (sink_) sink_("start"); return true; }
        if (c == "x") { if (sink_) sink_("stop"); return true; }
        if (c == "h") { if (sink_) sink_("set motorctl HOME"); return true; }
        if (c == "r") { if (sink_) sink_("set motorctl RESET"); return true; }
        if (c == ":") { command_mode = true; return true; }
        return false;
    });

    // ScreenInteractive can't be moved, so it lives on this thread's stack; the
    // atomic pointer lets stop()/refreshLoop() reach it.
    ScreenInteractive screen = ScreenInteractive::Fullscreen();
    screen_.store(&screen);
    if (running_) screen.Loop(root);
    screen_.store(nullptr);
}

}  // namespace fgc
