"use strict";

const requestLoadedCount = document.getElementById("request-loaded-count");
const requestLatestSeen = document.getElementById("request-latest-seen");
const requestEmpty = document.getElementById("request-empty");
const requestContent = document.getElementById("request-content");
const requestGroups = document.getElementById("request-groups");
const requestDetailSummary = document.getElementById("request-detail-summary");
const requestDetailJson = document.getElementById("request-detail-json");
const requestDetailCorrelation = document.getElementById(
  "request-detail-correlation"
);
const requestDetailFingerprintSource = document.getElementById(
  "request-detail-fingerprint-source"
);
const requestDetailTechnicalFields = document.getElementById(
  "request-detail-technical-fields"
);

let observedRequests = [];
let observedRequestGroups = [];
let selectedRequestFingerprint = null;

document.addEventListener("DOMContentLoaded", async () => {
  await renderObservedRequests();
});

async function renderObservedRequests() {
  try {
    const result = await browser.runtime.sendMessage({
      type: "list_recent_observed_requests"
    });

    if (!result?.success) {
      throw new Error(result?.error ?? "list_recent_observed_requests_failed");
    }

    observedRequests = result.observedRequests ?? [];
    observedRequestGroups = buildObservedRequestGroups(observedRequests);
    updateOverview();

    if (observedRequests.length === 0) {
      renderNoObservedRequests();
      return;
    }

    requestEmpty.hidden = true;
    requestContent.hidden = false;

    if (!findObservedRequest(selectedRequestFingerprint)) {
      selectedRequestFingerprint =
        observedRequests[0]?.requestFingerprint ?? null;
    }

    renderRequestGroups();
    renderSelectedRequest();
  } catch (error) {
    observedRequests = [];
    observedRequestGroups = [];
    updateOverview();
    requestEmpty.hidden = false;
    requestContent.hidden = true;
    requestEmpty.textContent =
      "Beobachtete Requests konnten nicht geladen werden.";
    console.warn("VG-Observe request list failed", error);
  }
}

function updateOverview() {
  requestLoadedCount.textContent = String(observedRequests.length);
  requestLatestSeen.textContent = formatNullable(
    observedRequests[0]?.lastSeenAt ?? null
  );
}

function renderNoObservedRequests() {
  selectedRequestFingerprint = null;
  requestGroups.textContent = "";
  clearRequestDetails();
  requestEmpty.hidden = false;
  requestContent.hidden = true;
  requestEmpty.textContent = "Keine beobachteten Requests vorhanden.";
}

function renderRequestGroups() {
  requestGroups.textContent = "";

  for (const group of observedRequestGroups) {
    requestGroups.append(buildRequestGroupElement(group));
  }
}

function buildRequestGroupElement(group) {
  const section = document.createElement("section");
  const title = document.createElement("h3");
  const context = document.createElement("dl");
  const tableWrap = document.createElement("div");
  const table = document.createElement("table");
  const head = document.createElement("thead");
  const headRow = document.createElement("tr");
  const body = document.createElement("tbody");

  section.className = "request-context-card";
  title.textContent = "Technisch beobachteter Vorgang";
  context.className = "request-context-summary";
  tableWrap.className = "request-list-wrap";
  table.className = "request-list";

  appendDefinition(context, "Seiten-/Dokumentkontext", group.contextLabel);
  appendDefinition(context, "Tab / Frame", group.tabFrameLabel);
  appendDefinition(context, "Zeitraum", group.periodLabel);
  appendDefinition(context, "Requests", String(group.requests.length));
  appendDefinition(
    context,
    "Empfaenger-Origins",
    String(group.recipientOriginCount)
  );
  appendDefinition(
    context,
    "Third-Party-Requests",
    String(group.thirdPartyCount)
  );
  appendDefinition(
    context,
    "Requests mit Consent-Parametern",
    String(group.consentParamCount)
  );
  appendDefinition(
    context,
    "Requests mit Consent-Korrelation",
    String(group.consentCorrelationCount)
  );

  [
    "Letzte Beobachtung",
    "Empfaenger-Origin",
    "Pfad",
    "Third-Party",
    "Consent-Parameter",
    "Consent-Korrelation",
    "SeenCount"
  ].forEach((label) => {
    const cell = document.createElement("th");

    cell.scope = "col";
    cell.textContent = label;
    headRow.append(cell);
  });

  for (const observedRequest of group.requests) {
    body.append(buildRequestRow(observedRequest));
  }

  head.append(headRow);
  table.append(head, body);
  tableWrap.append(table);
  section.append(title, context, tableWrap);

  return section;
}

function appendDefinition(list, label, value) {
  const item = document.createElement("div");
  const term = document.createElement("dt");
  const description = document.createElement("dd");

  term.textContent = label;
  description.textContent = value;
  item.append(term, description);
  list.append(item);
}

function buildRequestRow(observedRequest) {
  const row = document.createElement("tr");
  const isSelected =
    observedRequest?.requestFingerprint === selectedRequestFingerprint;

  row.className = isSelected ? "is-selected" : "";
  row.tabIndex = 0;
  row.setAttribute("role", "button");
  row.setAttribute("aria-pressed", isSelected ? "true" : "false");

  row.addEventListener("click", () => {
    selectObservedRequest(observedRequest?.requestFingerprint ?? null);
  });

  row.addEventListener("keydown", (event) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      selectObservedRequest(observedRequest?.requestFingerprint ?? null);
    }
  });

  appendListCell(row, formatNullable(observedRequest?.lastSeenAt));
  appendListCell(
    row,
    formatNullable(observedRequest?.request?.origin),
    "origin-cell"
  );
  appendListCell(
    row,
    formatNullable(observedRequest?.request?.pathname),
    "path-cell"
  );
  appendListCell(row, formatThirdParty(observedRequest?.request?.thirdParty));
  appendListCell(row, formatYesNo(hasConsentParams(observedRequest)));
  appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest)));
  appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric");

  return row;
}

function appendListCell(row, value, className) {
  const cell = document.createElement("td");

  if (className) {
    cell.className = className;
  }

  cell.textContent = value;
  row.append(cell);
}

function selectObservedRequest(requestFingerprint) {
  selectedRequestFingerprint = requestFingerprint;
  renderRequestGroups();
  renderSelectedRequest();
}

function renderSelectedRequest() {
  const observedRequest = findObservedRequest(selectedRequestFingerprint);

  clearRequestDetails();

  if (!observedRequest) {
    return;
  }

  renderSummaryTable([
    ["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)],
    ["Erste Beobachtung", formatNullable(observedRequest?.firstSeenAt)],
    ["Beobachtet um", formatNullable(getObservedAt(observedRequest))],
    ["Seiten-/Dokumentkontext", getPageContextLabel(observedRequest)],
    ["Tab-ID", formatUnknown(getTabId(observedRequest))],
    ["Frame-ID", formatUnknown(getFrameId(observedRequest))],
    ["Empfaenger-Origin", formatNullable(observedRequest?.request?.origin)],
    ["Pfad", formatNullable(observedRequest?.request?.pathname)],
    ["Methode", formatNullable(observedRequest?.request?.method)],
    ["Request-Typ", formatNullable(observedRequest?.request?.type)],
    ["Third-Party", formatThirdParty(observedRequest?.request?.thirdParty)],
    ["Consent-Parameter vorhanden", formatYesNo(hasConsentParams(observedRequest))],
    [
      "Consent-Korrelation vorhanden",
      formatYesNo(hasConsentCorrelation(observedRequest))
    ],
    ["SeenCount", formatNullable(observedRequest?.seenCount)],
    [
      "Request-Fingerprint",
      formatNullable(observedRequest?.requestFingerprint)
    ]
  ]);

  requestDetailJson.textContent = JSON.stringify(observedRequest, null, 2);
  requestDetailCorrelation.textContent = JSON.stringify(
    observedRequest?.correlation ?? null,
    null,
    2
  );
  requestDetailFingerprintSource.textContent = JSON.stringify(
    observedRequest?.requestFingerprintSource ?? null,
    null,
    2
  );
  requestDetailTechnicalFields.textContent = JSON.stringify(
    buildTechnicalFieldExtract(observedRequest),
    null,
    2
  );
}

function renderSummaryTable(rows) {
  const table = document.createElement("table");
  const body = document.createElement("tbody");

  table.className = "inspector-table";

  for (const [label, value] of rows) {
    const row = document.createElement("tr");
    const labelCell = document.createElement("th");
    const valueCell = document.createElement("td");

    labelCell.scope = "row";
    labelCell.textContent = label;
    valueCell.className = "inspector-value";
    valueCell.textContent = value;
    row.append(labelCell, valueCell);
    body.append(row);
  }

  table.append(body);
  requestDetailSummary.append(table);
}

function clearRequestDetails() {
  requestDetailSummary.textContent = "";
  requestDetailJson.textContent = "";
  requestDetailCorrelation.textContent = "";
  requestDetailFingerprintSource.textContent = "";
  requestDetailTechnicalFields.textContent = "";
}

function findObservedRequest(requestFingerprint) {
  if (!requestFingerprint) {
    return null;
  }

  return (
    observedRequests.find((observedRequest) => {
      return observedRequest?.requestFingerprint === requestFingerprint;
    }) ?? null
  );
}

function hasConsentParams(observedRequest) {
  const consentParams = observedRequest?.consentParams;

  if (!consentParams || typeof consentParams !== "object") {
    return false;
  }

  return Object.values(consentParams).some((value) => {
    return value !== null && value !== undefined && value !== "";
  });
}

function hasConsentCorrelation(observedRequest) {
  return Boolean(observedRequest?.correlation?.stateFingerprint);
}

function buildTechnicalFieldExtract(observedRequest) {
  return {
    schemaVersion: observedRequest?.schemaVersion ?? null,
    recordedAt: observedRequest?.recordedAt ?? null,
    recordingSource: observedRequest?.recordingSource ?? null,
    observedAt: observedRequest?.observedAt ?? null,
    firstSeenAt: observedRequest?.firstSeenAt ?? null,
    lastSeenAt: observedRequest?.lastSeenAt ?? null,
    seenCount: observedRequest?.seenCount ?? null,
    requestFingerprint: observedRequest?.requestFingerprint ?? null,
    requestFingerprintSource: observedRequest?.requestFingerprintSource ?? null,
    context: observedRequest?.context ?? null,
    request: observedRequest?.request ?? null,
    consentParams: observedRequest?.consentParams ?? null,
    correlation: observedRequest?.correlation ?? null
  };
}

function buildObservedRequestGroups(requests) {
  const groupsByKey = new Map();

  for (const observedRequest of requests) {
    const key = buildObservedRequestGroupKey(observedRequest);
    let group = groupsByKey.get(key);

    if (!group) {
      group = {
        key,
        contextLabel: getPageContextLabel(observedRequest),
        tabFrameLabel: buildTabFrameLabel(observedRequest),
        requests: []
      };
      groupsByKey.set(key, group);
    }

    group.requests.push(observedRequest);
  }

  return Array.from(groupsByKey.values())
    .map(enrichObservedRequestGroup)
    .sort((left, right) => {
      return compareTimestampValues(right.lastSeenAt, left.lastSeenAt);
    });
}

function buildObservedRequestGroupKey(observedRequest) {
  return [
    `tab:${formatKeyValue(getTabId(observedRequest))}`,
    `frame:${formatKeyValue(getFrameId(observedRequest))}`,
    `page:${getPageContextValue(observedRequest) ?? "unknown"}`
  ].join("|");
}

function enrichObservedRequestGroup(group) {
  const sortedRequests = [...group.requests].sort((left, right) => {
    return compareTimestampValues(getObservedAt(left), getObservedAt(right));
  });
  const recipientOrigins = new Set(
    sortedRequests
      .map((observedRequest) => observedRequest?.request?.origin ?? null)
      .filter((origin) => origin !== null && origin !== undefined && origin !== "")
  );
  let firstSeenAt = null;
  let lastSeenAt = null;

  for (const observedRequest of sortedRequests) {
    firstSeenAt = pickTimestamp(firstSeenAt, observedRequest?.firstSeenAt, "min");
    lastSeenAt = pickTimestamp(lastSeenAt, observedRequest?.lastSeenAt, "max");
  }

  return {
    ...group,
    requests: sortedRequests,
    firstSeenAt,
    lastSeenAt,
    periodLabel: buildPeriodLabel(firstSeenAt, lastSeenAt),
    recipientOriginCount: recipientOrigins.size,
    thirdPartyCount: sortedRequests.filter((observedRequest) => {
      return observedRequest?.request?.thirdParty === true;
    }).length,
    consentParamCount: sortedRequests.filter(hasConsentParams).length,
    consentCorrelationCount: sortedRequests.filter(hasConsentCorrelation).length
  };
}

function getPageContextLabel(observedRequest) {
  return formatUnknown(getPageContextValue(observedRequest));
}

function getPageContextValue(observedRequest) {
  return (
    getFirstPresentValue(
      observedRequest,
      [
        "pageUrl",
        "documentUrl",
        "initiator",
        "sourceUrl",
        "context.pageUrl",
        "context.documentUrl",
        "context.initiator",
        "context.sourceUrl",
        "request.pageUrl",
        "request.documentUrl",
        "request.initiator",
        "request.sourceUrl"
      ]
    ) ?? null
  );
}

function getTabId(observedRequest) {
  return getFirstPresentValue(observedRequest, ["tabId", "context.tabId"]);
}

function getFrameId(observedRequest) {
  return getFirstPresentValue(observedRequest, ["frameId", "context.frameId"]);
}

function getObservedAt(observedRequest) {
  return getFirstPresentValue(observedRequest, [
    "observedAt",
    "firstSeenAt",
    "lastSeenAt"
  ]);
}

function getFirstPresentValue(source, paths) {
  for (const path of paths) {
    const value = getPathValue(source, path);

    if (value !== null && value !== undefined && value !== "") {
      return value;
    }
  }

  return null;
}

function getPathValue(source, path) {
  return path.split(".").reduce((value, key) => {
    if (value === null || value === undefined || typeof value !== "object") {
      return undefined;
    }

    return value[key];
  }, source);
}

function buildTabFrameLabel(observedRequest) {
  return `Tab ${formatUnknown(getTabId(observedRequest))} / Frame ${formatUnknown(
    getFrameId(observedRequest)
  )}`;
}

function pickTimestamp(current, candidate, direction) {
  if (!current) {
    return candidate ?? null;
  }

  if (!candidate) {
    return current;
  }

  const comparison = compareTimestampValues(candidate, current);

  if (direction === "min") {
    return comparison < 0 ? candidate : current;
  }

  return comparison > 0 ? candidate : current;
}

function compareTimestampValues(left, right) {
  const leftTime = Date.parse(left ?? "");
  const rightTime = Date.parse(right ?? "");

  if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) {
    return 0;
  }

  if (Number.isNaN(leftTime)) {
    return -1;
  }

  if (Number.isNaN(rightTime)) {
    return 1;
  }

  return leftTime - rightTime;
}

function buildPeriodLabel(firstSeenAt, lastSeenAt) {
  if (!firstSeenAt && !lastSeenAt) {
    return "unbekannt";
  }

  if (firstSeenAt === lastSeenAt || !lastSeenAt) {
    return formatUnknown(firstSeenAt);
  }

  if (!firstSeenAt) {
    return formatUnknown(lastSeenAt);
  }

  return `${firstSeenAt} bis ${lastSeenAt}`;
}

function formatKeyValue(value) {
  return value === null || value === undefined || value === ""
    ? "unknown"
    : value;
}

function formatUnknown(value) {
  if (value === null || value === undefined || value === "") {
    return "unbekannt";
  }

  return String(value);
}

function formatNullable(value) {
  if (value === null || value === undefined || value === "") {
    return "unbekannt";
  }

  return String(value);
}

function formatThirdParty(value) {
  if (value === true) {
    return "ja";
  }

  if (value === false) {
    return "nein";
  }

  return "unbekannt";
}

function formatYesNo(value) {
  return value ? "vorhanden" : "nicht vorhanden";
}
