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";
import { useTranslation } from "react-i18next";

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

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

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

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

  // Inject JSON highlight styles once (only when colorize is enabled)
  useEffect(() => {
    if (!colorizeJson) return;
    if (typeof document === "undefined") return;
    let style = document.getElementById("json-highlight-styles");
    if (!style) {
      style = document.createElement("style");
      style.id = "json-highlight-styles";
      document.head.appendChild(style);
    }
    style.textContent = `
      .json-key { color: #fa8c16; }
      .json-string { color: #52c41a; }
      .json-number { color: #722ed1; }
      .json-boolean { color: #1890ff; }
      .json-null { color: #faad14; }
      .xml-tag { color: #1677ff; }
      .xml-attr { color: #d46b08; }
      .xml-value { color: #389e0d; }
      .xml-decl { color: #7c3aed; }
      .xml-comment { color: #8c8c8c; }
    `;
  }, [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);
        const xml = hasMeta && allowXmlPayload ? extractXmlFromMeta(meta) : { request: null, response: null };
        const hasRequestXml = !!xml.request;
        const hasResponseXml = !!xml.response;
        const copyPayload = hasMeta ? getCopyPayload(meta) : null;
        const copyPayloadKey = `copy-${idx}`;
        const copyReqKey = `copy-req-${idx}`;
        const copyResKey = `copy-res-${idx}`;

        return {
          key: idx,
          color: logLevelColor(level),
          content: (
            <Space orientation="vertical" size={4} style={{ display: "flex" }}>
              {/* Row 1: summary + inline "Details" toggle */}
              <Space wrap align="start">
                <Tag color={logLevelColor(level)}>{level}</Tag>
                <Divider orientation="vertical" />
                <span>{dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
                <Divider orientation="vertical" />
                <span>{message}</span>
                {hasMeta && (
                  <>
                    <Divider orientation="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", fontSize: 11 }}
                    >
                      {isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")}
                    </a>
                    <Divider orientation="vertical" />
                    <a
                      role="button"
                      onClick={() => handleCopyAction(copyPayloadKey, copyPayload, setCopiedKey)}
                      style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
                    >
                      {copiedKey === copyPayloadKey ? t("dms.labels.copied") : t("dms.labels.copy")}
                    </a>
                    {hasRequestXml && (
                      <>
                        <Divider orientation="vertical" />
                        <a
                          role="button"
                          onClick={() => handleCopyAction(copyReqKey, xml.request, setCopiedKey)}
                          style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
                        >
                          {copiedKey === copyReqKey ? t("dms.labels.copied") : t("dms.labels.copy_request")}
                        </a>
                      </>
                    )}
                    {hasResponseXml && (
                      <>
                        <Divider orientation="vertical" />
                        <a
                          role="button"
                          onClick={() => handleCopyAction(copyResKey, xml.response, setCopiedKey)}
                          style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
                        >
                          {copiedKey === copyResKey ? t("dms.labels.copied") : t("dms.labels.copy_response")}
                        </a>
                      </>
                    )}
                  </>
                )}
              </Space>

              {/* Row 2: details body (only when open) */}
              {hasMeta && isOpen && (
                <div style={{ marginLeft: 6 }}>
                  <JsonBlock isDarkMode={isDarkMode} data={removeXmlFromMeta(meta)} colorize={colorizeJson} />
                  {hasRequestXml && (
                    <XmlBlock
                      isDarkMode={isDarkMode}
                      title={t("dms.labels.request_xml")}
                      xmlText={xml.request}
                      colorize={colorizeJson}
                    />
                  )}
                  {hasResponseXml && (
                    <XmlBlock
                      isDarkMode={isDarkMode}
                      title={t("dms.labels.response_xml")}
                      xmlText={xml.response}
                      colorize={colorizeJson}
                    />
                  )}
                </div>
              )}
            </Space>
          )
        };
      }),
    [logs, openSet, colorizeJson, copiedKey, isDarkMode, showDetails, allowXmlPayload, t]
  );

  return <Timeline 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);
  }
};

/**
 * Get request/response XML from various Reynolds log meta shapes.
 * @param meta
 * @returns {{request: string|null, response: string|null}}
 */
const extractXmlFromMeta = (meta) => {
  const request =
    firstString(meta?.requestXml) ||
    firstString(meta?.xml?.request) ||
    firstString(meta?.response?.xml?.request) ||
    firstString(meta?.response?.requestXml);

  const response =
    firstString(meta?.responseXml) || firstString(meta?.xml?.response) || firstString(meta?.response?.xml?.response);

  return { request, response };
};

/**
 * Return the value to copy when clicking the "Copy" action.
 * @param meta
 * @returns {*}
 */
const getCopyPayload = (meta) => {
  if (meta?.payload != null) return meta.payload;
  return meta;
};

/**
 * Remove bulky XML fields from object shown in JSON block (XML is rendered separately).
 * @param meta
 * @returns {*}
 */
const removeXmlFromMeta = (meta) => {
  if (meta == null || typeof meta !== "object") return meta;
  const cloned = safeClone(meta);
  if (cloned == null || typeof cloned !== "object") return meta;

  if (typeof cloned.requestXml === "string") delete cloned.requestXml;
  if (typeof cloned.responseXml === "string") delete cloned.responseXml;

  if (cloned.xml && typeof cloned.xml === "object") {
    if (typeof cloned.xml.request === "string") delete cloned.xml.request;
    if (typeof cloned.xml.response === "string") delete cloned.xml.response;
    if (isEmpty(cloned.xml)) delete cloned.xml;
  }

  if (cloned.response?.xml && typeof cloned.response.xml === "object") {
    if (typeof cloned.response.xml.request === "string") delete cloned.response.xml.request;
    if (typeof cloned.response.xml.response === "string") delete cloned.response.xml.response;
    if (isEmpty(cloned.response.xml)) delete cloned.response.xml;
  }

  return cloned;
};

/**
 * Safe deep clone for plain JSON structures.
 * @param value
 * @returns {*}
 */
const safeClone = (value) => {
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return value;
  }
};

/**
 * First non-empty string helper.
 * @param value
 * @returns {string|null}
 */
const firstString = (value) => {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  return trimmed ? trimmed : null;
};

/**
 * Copy arbitrary text/object to clipboard.
 * @param key
 * @param value
 * @param setCopied
 * @returns {Promise<void>}
 */
const handleCopyAction = async (key, value, setCopied) => {
  const text = typeof value === "string" ? value : safeStringify(value, 2);
  if (!text) return;
  const copied = await copyTextToClipboard(text);
  if (!copied) return;
  setCopied(key);
  setTimeout(() => {
    setCopied((prev) => (prev === key ? null : prev));
  }, 1200);
};

/**
 * Clipboard helper (modern async Clipboard API).
 * @param text
 * @returns {Promise<boolean>}
 */
const copyTextToClipboard = async (text) => {
  if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
    return false;
  }
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return false;
  }
};

/**
 * 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>;
};

/**
 * XML display block with normalized indentation.
 * @param title
 * @param xmlText
 * @param isDarkMode
 * @returns {JSX.Element}
 * @constructor
 */
const XmlBlock = ({ title, xmlText, isDarkMode, colorize = false }) => {
  const base = {
    margin: "8px 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",
    whiteSpace: "pre"
  };

  return (
    <div style={{ marginTop: 6 }}>
      <div style={{ fontSize: 11, fontWeight: 600 }}>{title}</div>
      {colorize ? (
        <pre style={base} dangerouslySetInnerHTML={{ __html: highlightXml(formatXml(xmlText)) }} />
      ) : (
        <pre style={base}>{formatXml(xmlText)}</pre>
      )}
    </div>
  );
};

/**
 * Basic XML pretty-printer.
 * @param xml
 * @returns {string}
 */
const formatXml = (xml) => {
  if (typeof xml !== "string") return "";
  const normalized = xml.replace(/\r\n/g, "\n").replace(/>\s*</g, ">\n<").trim();
  const lines = normalized.split("\n");
  let indent = 0;
  const out = [];

  for (const rawLine of lines) {
    const line = rawLine.trim();
    if (!line) continue;

    if (/^<\/[^>]+>/.test(line)) indent = Math.max(indent - 1, 0);
    out.push(`${"  ".repeat(indent)}${line}`);

    const opens = (line.match(/<[^/!?][^>]*>/g) || []).length;
    const closes = (line.match(/<\/[^>]+>/g) || []).length;
    const selfClosing = (line.match(/<[^>]+\/>/g) || []).length;
    const declaration = /^<\?xml/.test(line) ? 1 : 0;

    indent += opens - closes - selfClosing - declaration;
    if (indent < 0) indent = 0;
  }

  return out.join("\n");
};

/**
 * Syntax highlight pretty-printed XML text for HTML display.
 * @param xmlText
 * @returns {string}
 */
const highlightXml = (xmlText) => {
  const esc = String(xmlText || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
  const lines = esc.split("\n");

  return lines
    .map((line) => {
      let out = line;

      out = out.replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="xml-comment">$1</span>');
      out = out.replace(/(&lt;\?xml[\s\S]*?\?&gt;)/g, '<span class="xml-decl">$1</span>');

      out = out.replace(/(&lt;\/?)([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?&gt;)/g, (_m, open, tag, attrs, close) => {
        const coloredAttrs = attrs.replace(
          /([A-Za-z_][\w:.-]*)(=)("[^"]*"|'[^']*'|&quot;[\s\S]*?&quot;|&apos;[\s\S]*?&apos;)/g,
          '<span class="xml-attr">$1</span>$2<span class="xml-value">$3</span>'
        );
        return `${open}<span class="xml-tag">${tag}</span>${coloredAttrs}${close}`;
      });

      return out;
    })
    .join("\n");
};

/**
 * 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>`;
    }
  );
};
