/**
 * Static prefix string used in every log entry to make browser console
 * filtering easier when multiple apps share the same window.
 */
const LOG_PREFIX = "[tauclock]";
/**
 * Permitted log levels ordered by severity so we can determine whether a
 * message should be printed by comparing their indexes.
 */
const LOG_LEVELS = ["error", "warn", "info", "debug"];
/**
 * Effective log level obtained either from localStorage, a query parameter or
 * the default, which then gates every log call in the frontend.
 */
const LOG_LEVEL = resolveLogLevel();

/**
 * JSDoc log wrapper that normalizes console usage and ensures we respect the
 * active log level before emitting potentially noisy diagnostic data.
 */
function logFrontend(level, message, details) {
  if (!shouldLog(level)) return;
  const output = `${LOG_PREFIX} ${message}`;
  const logger = console[level] || console.log;
  if (details === undefined) {
    logger.call(console, output);
  } else {
    logger.call(console, output, details);
  }
}

const logInfo = (message, details) => logFrontend("info", message, details);
const logWarn = (message, details) => logFrontend("warn", message, details);
const logDebug = (message, details) => logFrontend("debug", message, details);
const logError = (message, details) => logFrontend("error", message, details);

/**
 * Returns true if the requested log level is enabled by comparing the
 * requested level's index with the configured LOG_LEVEL bucket.
 */
function shouldLog(level) {
  const requested = LOG_LEVELS.indexOf(level);
  const active = LOG_LEVELS.indexOf(LOG_LEVEL);
  if (requested === -1 || active === -1) return false;
  return requested <= active;
}

/**
 * Determines the preferred log level by first checking localStorage, then the
 * `logLevel` URL parameter, and finally falling back to `info`.
 */
function resolveLogLevel() {
  const stored = window.localStorage?.getItem("tauclock:log-level");
  const normalizedStored = stored?.toLowerCase();
  if (normalizedStored && LOG_LEVELS.includes(normalizedStored)) return normalizedStored;
  if (window.location?.search?.includes("logLevel=")) {
    const params = new URLSearchParams(window.location.search);
    const requested = params.get("logLevel")?.toLowerCase();
    if (requested && LOG_LEVELS.includes(requested)) return requested;
  }
  return "info";
}

const invoke = resolveInvoke();

/**
 * Finds the best available Tauri invoke bridge and emits structured debug
 * information for troubleshooting. If the bridge is missing, a stub is
 * returned so the UI can fail loudly rather than silently.
 */
function resolveInvoke() {
  const tauriGlobal = window.__TAURI__ || {};
  const candidates = [
    { fn: tauriGlobal?.core?.invoke, ctx: tauriGlobal?.core, namespace: "core" },
    { fn: tauriGlobal?.tauri?.invoke, ctx: tauriGlobal?.tauri, namespace: "tauri" },
    { fn: tauriGlobal.invoke, ctx: tauriGlobal, namespace: "root" },
  ].filter(({ fn }) => typeof fn === "function");
  if (candidates.length) {
    const { fn, ctx, namespace } = candidates[0];
    logDebug("Resolved Tauri invoke bridge", { namespace });
    return fn.bind(ctx);
  }
  logWarn("Tauri bridge was not found on window.__TAURI__");
  return async (command) => {
    const error = new Error("Tauri backend interface is unavailable");
    logError(`Attempted to invoke '${command}' but the Tauri bridge is missing`, error);
    throw error;
  };
}

/** Cache of DOM nodes keyed by logical role identifiers. */
const elements = {};
let tickInFlight = false;
let latestState = null;
let aboutInfo = null;

/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */
const SHORTCUTS = {
  KeyA: () => focusAndSelect(elements.alarmTime),
  KeyN: () => elements.alarmForm.requestSubmit(),
  KeyT: () => focusAndSelect(elements.timerInput),
  KeyG: () => triggerTimerToggle(false),
  KeyR: () => requestState("reset_timer"),
  KeyW: () => requestState("toggle_stopwatch"),
  KeyL: () => requestState("add_lap"),
  KeyP: () => requestState("reset_stopwatch"),
  KeyH: () => toggleShortcutOverlay(),
  KeyI: () => toggleAboutOverlay(),
};

window.addEventListener("DOMContentLoaded", () => {
  logInfo("DOM ready, initializing app");
  cacheElements();
  bindEvents();
  bootstrap();
});

/**
 * Finds and stores all the DOM nodes we need to reference frequently so we
 * avoid repeated lookups every render.
 */
function cacheElements() {
  elements.localTime = document.getElementById("local-time");
  elements.localDate = document.getElementById("local-date");
  elements.utcTime = document.getElementById("utc-time");
  elements.utcDate = document.getElementById("utc-date");
  elements.alarmForm = document.getElementById("alarm-form");
  elements.alarmTime = document.getElementById("alarm-time");
  elements.alarmLabel = document.getElementById("alarm-label");
  elements.alarmList = document.getElementById("alarm-list");
  elements.nextAlarm = document.getElementById("next-alarm");
  elements.timerInput = document.getElementById("timer-input");
  elements.timerDisplay = document.getElementById("timer-display");
  elements.timerToggle = document.getElementById("timer-toggle");
  elements.timerReset = document.getElementById("timer-reset");
  elements.timerStatus = document.getElementById("timer-status");
  elements.stopwatchDisplay = document.getElementById("stopwatch-display");
  elements.stopwatchToggle = document.getElementById("stopwatch-toggle");
  elements.stopwatchLap = document.getElementById("stopwatch-lap");
  elements.stopwatchReset = document.getElementById("stopwatch-reset");
  elements.stopwatchLaps = document.getElementById("stopwatch-laps");
  elements.statusMessage = document.getElementById("status-message");
  elements.shortcutOverlay = document.getElementById("shortcut-overlay");
  elements.shortcutClose = document.getElementById("shortcut-close");
  elements.shortcutTrigger = document.getElementById("shortcut-trigger");
  elements.aboutOverlay = document.getElementById("about-overlay");
  elements.aboutClose = document.getElementById("about-close");
  elements.aboutTrigger = document.getElementById("about-trigger");
  elements.aboutName = document.getElementById("about-name");
  elements.aboutVersion = document.getElementById("about-version");
  elements.aboutIdentifier = document.getElementById("about-identifier");
  elements.aboutLicenseName = document.getElementById("about-license-name");
  elements.aboutLicenseText = document.getElementById("about-license-text");
  const missing = Object.entries(elements)
    .filter(([, value]) => !value)
    .map(([key]) => key);
  if (missing.length) {
    logWarn("Some expected DOM nodes were not found", { missing });
  } else {
    logDebug("All DOM nodes cached successfully", { count: Object.keys(elements).length });
  }
}

/**
 * Hooks up all DOM events, including keyboard shortcuts, button clicks, and
 * form submissions, translating them into backend commands where needed.
 */
function bindEvents() {
  elements.alarmForm.addEventListener("submit", async (event) => {
    event.preventDefault();
    const beforeCount = latestState?.alarms?.items?.length ?? 0;
    const state = await requestState("create_alarm", {
      time: elements.alarmTime.value,
      label: elements.alarmLabel.value,
    });
    const afterCount = state?.alarms?.items?.length ?? beforeCount;
    if (afterCount > beforeCount) {
      elements.alarmForm.reset();
    }
  });

  elements.alarmList.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-action]");
    if (!button) return;
    const id = button.dataset.id;
    const action = button.dataset.action;
    if (action === "toggle") requestState("toggle_alarm", { id });
    if (action === "delete") requestState("delete_alarm", { id });
  });

  elements.timerToggle.addEventListener("click", () => triggerTimerToggle(false));
  elements.timerReset.addEventListener("click", () => requestState("reset_timer"));
  elements.timerInput.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      event.preventDefault();
      triggerTimerToggle(true);
    }
  });

  elements.stopwatchToggle.addEventListener("click", () => requestState("toggle_stopwatch"));
  elements.stopwatchLap.addEventListener("click", () => requestState("add_lap"));
  elements.stopwatchReset.addEventListener("click", () => requestState("reset_stopwatch"));

  elements.shortcutClose.addEventListener("click", () => toggleShortcutOverlay(false));
  elements.shortcutOverlay.addEventListener("click", (event) => {
    if (event.target === elements.shortcutOverlay) toggleShortcutOverlay(false);
  });
  if (elements.shortcutTrigger) {
    elements.shortcutTrigger.addEventListener("click", () => toggleShortcutOverlay(true));
  }
  elements.aboutClose.addEventListener("click", () => toggleAboutOverlay(false));
  elements.aboutOverlay.addEventListener("click", (event) => {
    if (event.target === elements.aboutOverlay) toggleAboutOverlay(false);
  });
  if (elements.aboutTrigger) {
    elements.aboutTrigger.addEventListener("click", () => toggleAboutOverlay(true));
  }

  document.addEventListener("keydown", handleShortcuts, true);
  logInfo("Core UI events bound");
}

/**
 * Performs the initial state fetch and schedules the recurring `tick` loop so
 * that the UI stays synchronized with the backend clock.
 */
async function bootstrap() {
  logInfo("Bootstrapping state");
  await Promise.all([requestState("get_state"), loadAboutInfo()]);
  setInterval(runTick, 250);
  logInfo("Tick loop scheduled", { intervalMs: 250 });
}

/** Loads application metadata used by the About dialog. */
async function loadAboutInfo() {
  try {
    aboutInfo = await invoke("get_about_info");
    renderAboutInfo(aboutInfo);
  } catch (error) {
    logError("Failed to load About information", error);
  }
}

/**
 * Requests a timer toggle from the backend, optionally forcing a reparse of
 * the duration input if the user explicitly pressed Enter inside the field.
 */
function triggerTimerToggle(forceParse) {
  logDebug("Timer toggle requested", { forceParse, input: elements.timerInput.value });
  requestState("toggle_timer", {
    input: elements.timerInput.value,
    forceParse,
  });
}

/**
 * Generic wrapper around the Tauri invoke API that logs both the request and
 * response, applies the new state snapshot, and gracefully handles failures.
 */
async function requestState(command, payload = {}) {
  try {
    logInfo(`Invoking backend command: ${command}`, payload);
    const state = await invoke(command, payload);
    logDebug(`Command ${command} succeeded`, {
      hasClock: Boolean(state?.clock),
      hasStatus: Boolean(state?.status),
    });
    applyState(state);
    return state;
  } catch (error) {
    logError(`Command ${command} failed`, error);
    return null;
  }
}

/**
 * Polling task that asks the backend for a light-weight state update so that
 * timers, alarms, and stopwatches continue to advance even without user
 * interaction. Re-entrancy is avoided via `tickInFlight`.
 */
async function runTick() {
  if (tickInFlight) {
    logDebug("Tick skipped because a previous request is still pending");
    return;
  }
  tickInFlight = true;
  try {
    logDebug("Requesting tick update");
    const state = await invoke("tick");
    logDebug("Tick update received", { localTime: state?.clock?.local_time });
    applyState(state);
  } catch (error) {
    logError("Tick failed", error);
  } finally {
    tickInFlight = false;
  }
}

/**
 * Applies a backend-provided snapshot to every UI subsystem (clock, alarms,
 * timer, stopwatch, and status messaging). Also plays the chime if requested.
 */
function applyState(state) {
  if (!state) return;
  latestState = state;
  logDebug("Applying state snapshot", {
    nextAlarm: state.alarms?.next_summary,
    timerStatus: state.timer?.status_text,
    shouldChime: state.should_chime,
  });
  updateClock(state.clock);
  renderAlarms(state.alarms);
  renderTimer(state.timer);
  renderStopwatch(state.stopwatch);
  renderStatus(state.status);
  if (state.should_chime) {
    playChime();
  }
}

/** Re-renders the clock labels for both local time and UTC. */
function updateClock(clock) {
  if (!clock) return;
  elements.localTime.textContent = clock.local_time;
  elements.localDate.textContent = clock.local_date;
  elements.utcTime.textContent = clock.utc_time;
  elements.utcDate.textContent = clock.utc_date;
}

/**
 * Renders the alarm list including badges, actions, and empty states, while
 * also surfacing the next upcoming alarm summary.
 */
function renderAlarms(section) {
  const list = elements.alarmList;
  list.innerHTML = "";
  if (!section || !section.items.length) {
    const empty = document.createElement("li");
    empty.textContent = "アラームはまだありません";
    list.appendChild(empty);
  } else {
    section.items.forEach((alarm) => {
      const li = document.createElement("li");
      if (!alarm.active) li.classList.add("muted");
      if (alarm.ringing) li.classList.add("ringing");
      const info = document.createElement("div");
      const title = document.createElement("strong");
      title.textContent = `${alarm.time_label} - ${alarm.label}`;
      const badge = document.createElement("span");
      badge.className = "badge";
      badge.textContent = alarm.active ? "ON" : "OFF";
      const next = document.createElement("small");
      next.textContent = `次回: ${alarm.next_relative}`;
      info.append(title, badge, next);
      const actions = document.createElement("div");
      actions.className = "alarm-actions";
      const toggleBtn = document.createElement("button");
      toggleBtn.type = "button";
      toggleBtn.dataset.action = "toggle";
      toggleBtn.dataset.id = alarm.id;
      toggleBtn.textContent = alarm.active ? "停止" : "起動";
      const deleteBtn = document.createElement("button");
      deleteBtn.type = "button";
      deleteBtn.dataset.action = "delete";
      deleteBtn.dataset.id = alarm.id;
      deleteBtn.textContent = "削除";
      actions.append(toggleBtn, deleteBtn);
      li.append(info, actions);
      list.appendChild(li);
    });
  }
  elements.nextAlarm.textContent = section ? section.next_summary : "";
}

/** Reflects the backend timer information in the UI controls and labels. */
function renderTimer(view) {
  if (!view) return;
  elements.timerDisplay.textContent = view.display;
  elements.timerToggle.textContent = view.toggle_label;
  elements.timerStatus.textContent = view.status_text || "";
}

/** Displays stopwatch totals, the toggle label, and lap entries. */
function renderStopwatch(view) {
  if (!view) return;
  elements.stopwatchDisplay.textContent = view.display;
  elements.stopwatchToggle.textContent = view.toggle_label;
  const list = elements.stopwatchLaps;
  list.innerHTML = "";
  if (!view.laps.length) {
    const empty = document.createElement("li");
    empty.textContent = "ラップなし";
    list.appendChild(empty);
    return;
  }
  view.laps.forEach((lap) => {
    const li = document.createElement("li");
    li.dataset.id = lap.id;
    const label = document.createElement("strong");
    label.textContent = lap.label;
    const total = document.createElement("small");
    total.textContent = lap.total;
    const delta = document.createElement("small");
    delta.textContent = lap.delta;
    li.append(label, total, delta);
    list.appendChild(li);
  });
}

/** Shows transient status messages returned by the backend. */
function renderStatus(message) {
  if (!elements.statusMessage) return;
  elements.statusMessage.textContent = message || "";
}

/** Focuses a control and selects its text when possible for easy overwriting. */
function focusAndSelect(element) {
  if (!element) return;
  element.focus();
  if (typeof element.select === "function") element.select();
}

/**
 * Toggles the keyboard shortcut cheat sheet overlay, optionally forcing the
 * visibility state when invoked with a boolean.
 */
function toggleShortcutOverlay(force) {
  const overlay = elements.shortcutOverlay;
  if (!overlay) return;
  const next = typeof force === "boolean" ? force : overlay.hidden;
  if (next) {
    toggleAboutOverlay(false);
  }
  overlay.hidden = !next;
  logInfo("Shortcut overlay toggled", { visible: next });
  if (next) {
    overlay.querySelector(".shortcut-card")?.focus?.();
  }
}

/**
 * Toggles the About dialog visibility and lazily refreshes metadata when
 * missing so the UI can recover from early load failures.
 */
function toggleAboutOverlay(force) {
  const overlay = elements.aboutOverlay;
  if (!overlay) return;
  const next = typeof force === "boolean" ? force : overlay.hidden;
  if (next) {
    toggleShortcutOverlay(false);
    if (!aboutInfo) {
      loadAboutInfo();
    }
  }
  overlay.hidden = !next;
  logInfo("About overlay toggled", { visible: next });
  if (next) {
    overlay.querySelector(".about-card")?.focus?.();
  }
}

/** Applies About metadata fields into the dialog. */
function renderAboutInfo(info) {
  if (!info) return;
  elements.aboutName.textContent = info.app_name || "-";
  elements.aboutVersion.textContent = info.version || "-";
  elements.aboutIdentifier.textContent = info.identifier || "-";
  elements.aboutLicenseName.textContent = info.license_name || "-";
  elements.aboutLicenseText.textContent = info.license_text || "";
}

/**
 * Global keyboard listener that translates single-key shortcuts into higher
 * level actions while respecting modifier keys, focus states, and overlays.
 */
function handleShortcuts(event) {
  if (event.defaultPrevented) return;
  const shortcutOverlayVisible = elements.shortcutOverlay && !elements.shortcutOverlay.hidden;
  const aboutOverlayVisible = elements.aboutOverlay && !elements.aboutOverlay.hidden;
  const overlayVisible = shortcutOverlayVisible || aboutOverlayVisible;
  const trackedKey = Boolean(SHORTCUTS[event.code]) || event.key === "Escape";
  if (trackedKey) {
    logDebug("Shortcut event received", {
      code: event.code,
      overlayVisible,
      hasModifier: event.altKey || event.metaKey || event.ctrlKey,
      targetTag: event.target?.tagName,
    });
  }
  if (event.key === "Escape") {
    if (shortcutOverlayVisible) {
      toggleShortcutOverlay(false);
      event.preventDefault();
      return;
    }
    if (aboutOverlayVisible) {
      toggleAboutOverlay(false);
      event.preventDefault();
      return;
    }
    if (document.activeElement && document.activeElement !== document.body) {
      document.activeElement.blur();
      event.preventDefault();
    }
    return;
  }
  if (event.altKey || event.metaKey || event.ctrlKey) return;
  if (overlayVisible && event.code !== "KeyH" && event.code !== "KeyI") return;
  if (isTypingTarget(event.target)) return;
  const handler = SHORTCUTS[event.code];
  if (handler) {
    event.preventDefault();
    logInfo("Keyboard shortcut triggered", { code: event.code });
    handler();
  }
}

/** Returns true when the event target is an editable/typing-friendly element. */
function isTypingTarget(target) {
  if (!target) return false;
  const element = target instanceof Element ? target : target.parentElement;
  if (!element) return false;
  if (element.isContentEditable) return true;
  return Boolean(element.closest("input, textarea, select, [contenteditable='true']"));
}

/**
 * Plays a very small synthesized chime using the Web Audio API in order to
 * alert the user even when no native notifications are available.
 */
function playChime(patternName = "alarm") {
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  if (!AudioCtx) return;
  playChime.ctx = playChime.ctx || new AudioCtx();
  if (playChime.ctx.state === "suspended") {
    playChime.ctx.resume();
  }
  const ctx = playChime.ctx;
  const patterns = {
    short: [660, 880, 660],
    alarm: [523, 659, 784, 659, 523],
  };
  const pattern = patterns[patternName] || patterns.alarm;
  let startTime = ctx.currentTime;
  pattern.forEach((frequency) => {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = frequency;
    osc.connect(gain);
    gain.connect(ctx.destination);
    gain.gain.setValueAtTime(0.0001, startTime);
    gain.gain.exponentialRampToValueAtTime(0.25, startTime + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.0001, startTime + 0.3);
    osc.start(startTime);
    osc.stop(startTime + 0.32);
    startTime += 0.35;
  });
}
