import { Divider, Space, Tag, Timeline } from "antd";
import { useEffect, useMemo, useState } from "react";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectDarkMode } from "../../redux/application/application.selectors.js";

const mapStateToProps = createStructuredSelector({
  isDarkMode: selectDarkMode
});

const mapDispatchToProps = () => ({});

export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);

export function DmsLogEvents({
  logs,
  detailsOpen,
  detailsNonce,
  isDarkMode,
  colorizeJson = false,
  showDetails = true
}) {
  const [openSet, setOpenSet] = useState(() => new Set());

  // Inject JSON highlight styles once (only when colorize is enabled)
  useEffect(() => {
    if (!colorizeJson) return;
    if (typeof document === "undefined") return;
    if (document.getElementById("json-highlight-styles")) return;
    const style = document.createElement("style");
    style.id = "json-highlight-styles";
    style.textContent = `
      .json-key { color: #fa8c16; }
      .json-string { color: #52c41a; }
      .json-number { color: #722ed1; }
      .json-boolean { color: #1890ff; }
      .json-null { color: #faad14; }
    `;
    document.head.appendChild(style);
  }, [colorizeJson]);

  // Trim openSet if logs shrink
  useEffect(() => {
    const len = (logs || []).length;
    setOpenSet((prev) => {
      const next = new Set();
      for (let i = 0; i < len; i++) if (prev.has(i)) next.add(i);
      return next;
    });
  }, [logs?.length]);

  // Respond to global toggle button
  useEffect(() => {
    if (detailsNonce == null) return;
    const len = (logs || []).length;
    setOpenSet(detailsOpen ? new Set(Array.from({ length: len }, (_, i) => i)) : new Set());
  }, [detailsNonce, detailsOpen, logs?.length]);

  const items = useMemo(
    () =>
      (logs || []).map((raw, idx) => {
        const { level, message, timestamp, meta } = normalizeLog(raw);

        // Only treat meta as "present" when we are allowed to show details
        const hasMeta = !isEmpty(meta) && showDetails;
        const isOpen = hasMeta && openSet.has(idx);

        return {
          key: idx,
          color: logLevelColor(level),
          children: (
            <Space direction="vertical" size={4} style={{ display: "flex" }}>
              {/* Row 1: summary + inline "Details" toggle */}
              <Space wrap align="start">
                <Tag color={logLevelColor(level)}>{level}</Tag>
                <Divider type="vertical" />
                <span>{dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
                <Divider type="vertical" />
                <span>{message}</span>
                {hasMeta && (
                  <>
                    <Divider type="vertical" />
                    <a
                      role="button"
                      aria-expanded={isOpen}
                      onClick={() =>
                        setOpenSet((prev) => {
                          const next = new Set(prev);
                          if (isOpen) next.delete(idx);
                          else next.add(idx);
                          return next;
                        })
                      }
                      style={{ cursor: "pointer", userSelect: "none" }}
                    >
                      {isOpen ? "Hide details" : "Details"}
                    </a>
                  </>
                )}
              </Space>

              {/* Row 2: details body (only when open) */}
              {hasMeta && isOpen && (
                <div style={{ marginLeft: 6 }}>
                  <JsonBlock isDarkMode={isDarkMode} data={meta} colorize={colorizeJson} />
                </div>
              )}
            </Space>
          )
        };
      }),
    [logs, openSet, colorizeJson, isDarkMode, showDetails]
  );

  return <Timeline pending reverse items={items} />;
}

/**
 * Normalize various log input formats into a standard structure.
 * @param input
 * @returns {{level: string, message: *|string, timestamp: Date, meta: *}}
 */
const normalizeLog = (input) => {
  const n = input?.normalized || input || {};
  const level = (n.level || input?.level || "INFO").toString().toUpperCase();
  const message = n.message ?? input?.message ?? "";
  const meta = input?.meta != null ? input.meta : n.meta != null ? n.meta : undefined;
  const tsRaw = input?.timestamp ?? n.timestamp ?? input?.ts ?? Date.now();
  const timestamp = typeof tsRaw === "number" ? new Date(tsRaw) : new Date(tsRaw);
  return { level, message, timestamp, meta };
};

/**
 * Map log level to tag color.
 * @param level
 * @returns {string}
 */
const logLevelColor = (level) => {
  switch ((level || "").toUpperCase()) {
    case "SILLY":
      return "purple";
    case "DEBUG":
      return "orange";
    case "INFO":
      return "blue";
    case "WARN":
    case "WARNING":
      return "yellow";
    case "ERROR":
      return "red";
    default:
      return "default";
  }
};

/**
 * Check if a value is "empty" (null/undefined, empty array, or empty object).
 * @param v
 */
const isEmpty = (v) => {
  if (v == null) return true;
  if (Array.isArray(v)) return v.length === 0;
  if (typeof v === "object") return Object.keys(v).length === 0;
  return false;
};

/**
 * Safely stringify an object to JSON, falling back to String() on failure.
 * @param obj
 * @param spaces
 * @returns {string}
 */
const safeStringify = (obj, spaces = 2) => {
  try {
    return JSON.stringify(obj, null, spaces);
  } catch {
    return String(obj);
  }
};

/**
 * JSON display block with optional syntax highlighting.
 * @param data
 * @param colorize
 * @param isDarkMode
 * @returns {JSX.Element}
 * @constructor
 */
const JsonBlock = ({ data, colorize, isDarkMode }) => {
  const jsonText = safeStringify(data, 2);
  const preStyle = {
    margin: "6px 0 0",
    maxWidth: 720,
    overflowX: "auto",
    fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
    fontSize: 12,
    lineHeight: 1.45,
    padding: 8,
    borderRadius: 6,
    background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
    border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
    color: isDarkMode ? "var(--card-text-fallback)" : "#141414"
  };

  if (colorize) {
    const html = syntaxHighlight(jsonText);
    return <pre style={preStyle} dangerouslySetInnerHTML={{ __html: html }} />;
  }
  return <pre style={preStyle}>{jsonText}</pre>;
};

/**
 * Syntax highlight JSON text for HTML display.
 * @param jsonText
 * @returns {*}
 */
const syntaxHighlight = (jsonText) => {
  const esc = jsonText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  return esc.replace(
    /("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
    (match) => {
      let cls = "json-number";
      if (match.startsWith('"')) {
        cls = match.endsWith(":") ? "json-key" : "json-string";
      } else if (match === "true" || match === "false") {
        cls = "json-boolean";
      } else if (match === "null") {
        cls = "json-null";
      }
      return `<span class="${cls}">${match}</span>`;
    }
  );
};
