import { useCallback, useEffect, useRef } from 'react';
import { useLocation } from 'react-router';
import type { PageTitleOverride } from 'officerdev';
import { usePageTitleOverride } from 'officerdev';
import { useSessionState, writeSessionValue } from 'hooks/useSessionState';
import { randomId } from 'helpers/random-id';

type TitleRule = { match: (p: string) => boolean; title: string };

// Default page title per route — most specific first.
const RULES: TitleRule[] = [
  { match: (p) => p === '/', title: 'Home' },
  { match: (p) => p.startsWith('/settings/ai'), title: 'AI Settings' },
  { match: (p) => p.startsWith('/settings/profile'), title: 'Profile' },
  { match: (p) => p.startsWith('/settings/integrations'), title: 'Integrations' },
  { match: (p) => p.startsWith('/settings/user-management'), title: 'User Management' },
  { match: (p) => p.startsWith('/settings/system'), title: 'System' },
  { match: (p) => p.startsWith('/settings'), title: 'Settings' },
  { match: (p) => p.startsWith('/chat'), title: 'Chat' },
  { match: (p) => p.startsWith('/email'), title: 'Email' },
  { match: (p) => p.startsWith('/files'), title: 'Files' },
  { match: (p) => p.startsWith('/calendar'), title: 'Calendar' },
  { match: (p) => p.startsWith('/contacts'), title: 'Contacts' },
  { match: (p) => p.startsWith('/app-store'), title: 'App store' },
  { match: (p) => p.startsWith('/plugins'), title: 'Plugins' },
  { match: (p) => p.startsWith('/photos'), title: 'Photos' },
  { match: (p) => p.startsWith('/jellyfin'), title: 'Video' },
  { match: (p) => p.startsWith('/soulseek'), title: 'Soulseek' },
  { match: (p) => p.startsWith('/transmission'), title: 'Transmission' },
  { match: (p) => p.startsWith('/gitea'), title: 'Gitea' },
  { match: (p) => p.startsWith('/invoices'), title: 'Invoices' },
  { match: (p) => p.startsWith('/wallet'), title: 'Wallet' },
  { match: (p) => p.startsWith('/system-monitor'), title: 'System Monitor' },
  { match: (p) => p.startsWith('/qr-transfer'), title: 'QR Transfer' },
  { match: (p) => p.startsWith('/activity'), title: 'Activity' },
  { match: (p) => p.startsWith('/code-editor'), title: 'Code Editor' },
  { match: (p) => p.startsWith('/tasks'), title: 'Tasks' },
  { match: (p) => p.startsWith('/jobs'), title: 'Jobs' },
  { match: (p) => p.startsWith('/skills'), title: 'Skills' },
  { match: (p) => p.startsWith('/processes'), title: 'Processes' },
  { match: (p) => p.startsWith('/dashboards'), title: 'Dashboards' },
  { match: (p) => p.startsWith('/terminal'), title: 'Terminal' },
  { match: (p) => p.startsWith('/browser'), title: 'Browser' },
  { match: (p) => p.startsWith('/desktop'), title: 'Desktop' },
];

export function titleForPath(pathname: string): string {
  return RULES.find((r) => r.match(pathname))?.title ?? 'Officer';
}

// ── The name you gave this tab ──
// There is no tab identity in the DOM: `tabId` is a privileged extension API, and page scripts are
// deliberately not allowed to know which tab they are in or that other tabs exist. We don't need one —
// **sessionStorage is already the per-tab store.** It is separate per tab, survives a refresh and
// in-place navigation, and is discarded when the tab closes, which is exactly the lifetime of a tab
// name. localStorage would be wrong in the obvious way: every tab would share one name.
//
// `useSessionState` is that pairing as a hook (see `hooks/useSessionState`); the name is one of its
// consumers. The tab *id* below stays on raw storage because it is claimed at module load, before any
// component exists to hold it.

const TAB_LABEL_KEY = 'OFFICER_TAB_LABEL';
const TAB_ID_KEY = 'OFFICER_TAB_ID';
const IDENTITY_CHANNEL = 'officer-tab-identity';

/** Storage can throw outright (private mode, storage disabled). A tab id is not worth a crash. */
function readStored(key: string): string | null {
  try {
    return sessionStorage.getItem(key);
  } catch {
    return null;
  }
}

function writeStored(key: string, value: string): void {
  try {
    sessionStorage.setItem(key, value);
  } catch {
    /* the value just doesn't persist */
  }
}

const labelDroppedHandlers = new Set<() => void>();

/** The clone check answers late, so React may already be showing the inherited name when it lands. */
function onTabLabelDropped(handler: () => void): () => void {
  labelDroppedHandlers.add(handler);
  return () => labelDroppedHandlers.delete(handler);
}

/**
 * Duplicating a tab **clones its sessionStorage**, so the copy would open wearing the original's name —
 * two tabs called "Platform Arch", which is precisely what naming one was meant to prevent. Same for a
 * tab opened by `window.open` from inside the app.
 *
 * This used to key off `navigation.type`, on the theory that a duplicate is a fresh navigation and a
 * refresh is not. It is not that simple: **only F5/Ctrl-R reports `reload`.** Pressing Enter in the
 * address bar, following a link back into the app, and re-opening the URL after the server was down are
 * all `navigate`, so every one of them threw the name away. Losing a name you typed is far worse than a
 * copy keeping one.
 *
 * So ask instead of guess. Each tab holds an id beside the name, and a copy is a tab whose id is *still
 * held by a tab that is alive* — which the original can simply say, over a BroadcastChannel. A refresh
 * has nobody to answer: the old document is gone before the new one's scripts run.
 *
 * Fails soft, in the direction of keeping the name: no channel, or nobody answering, means no clone.
 */
function claimTabIdentity(): void {
  let tabId = readStored(TAB_ID_KEY);
  if (!tabId) {
    // A tab with no id has nothing inherited to disown — it cannot be a copy of anything.
    writeStored(TAB_ID_KEY, newTabId());
    return;
  }

  let channel: BroadcastChannel;
  try {
    channel = new BroadcastChannel(IDENTITY_CHANNEL);
  } catch {
    return;
  }

  // Kept open for the life of the document: this tab is also the one that answers for its own id later.
  channel.onmessage = (ev: MessageEvent<{ kind: 'claim' | 'taken'; tabId: string }>) => {
    if (ev.data?.tabId !== tabId) return;
    if (ev.data.kind === 'claim') {
      channel.postMessage({ kind: 'taken', tabId });
      return;
    }
    // Someone alive is already this tab, so we are the copy. Take a new id and give up the name.
    tabId = newTabId();
    writeStored(TAB_ID_KEY, tabId);
    writeSessionValue<string | null>(TAB_LABEL_KEY, null);
    for (const handler of labelDroppedHandlers) handler();
  };
  channel.postMessage({ kind: 'claim', tabId });
}

/** `randomUUID` needs a secure context; the id only has to be unique among open tabs. */
function newTabId(): string {
  return randomId();
}

// Once per document, before React reads the stored name.
claimTabIdentity();

/**
 * The title shown in the header and in the browser tab, plus the setter behind the header's edit.
 *
 * A name you type is a property of the **tab**, not of the page, so it outlives navigation as well as a
 * refresh: you named this window to find it again among a dozen others, and wiping it because you opened
 * a different screen would defeat the point. Clear the field to hand the tab back to the route's own
 * name — that is the only way out, and there is no third state to get stuck in.
 *
 * Between the two sits a screen's own name for itself, published by whatever is showing (see
 * `usePublishPageTitle`): on `/chat/<id>` that is the conversation's title. It ranks under a typed name
 * for the reason above, and over the route default because "Chat" says less than the chat's name does.
 */
export function usePageTitle() {
  const { pathname } = useLocation();
  const override = usePageTitleOverride();
  const [label, setLabel] = useSessionState<string | null>(TAB_LABEL_KEY, null);

  useEffect(() => onTabLabelDropped(() => setLabel(null)), [setLabel]);

  /**
   * A rename outranks a tab name you typed earlier; opening a different chat does not.
   *
   * The precedence below makes a typed name permanent, which is right for navigation — you named this
   * window to find it again — and wrong the moment you rename the conversation itself. That was a
   * deliberate act on the same thing the tab is showing, and it appeared to do nothing: the tab kept the
   * old name, and kept it across reloads, because the stale one is in sessionStorage.
   *
   * The two are told apart by the id, which is why the override carries one. Same id and a new title is
   * a rename, and the newer act wins. A new id is navigation, and the tab name survives it.
   */
  const seenRef = useRef<PageTitleOverride | null>(null);
  useEffect(() => {
    const seen = seenRef.current;
    seenRef.current = override;
    if (!override || !seen) return;
    if (seen.id === override.id && seen.title !== override.title) setLabel(null);
  }, [override, setLabel]);

  const rename = useCallback((next: string) => setLabel(next.trim() || null), [setLabel]);

  return [label ?? override?.title ?? titleForPath(pathname), rename] as const;
}

/**
 * Mount once in the dashboard layout: mirror the title into the browser tab.
 *
 * There used to be a second effect resetting the title to the route default on every navigation, which
 * is what made a typed name last only until you clicked something. The route default is derived now
 * rather than assigned, so navigation retitles the tab by itself when no name is set, and leaves it
 * alone when one is.
 */
export function usePageTitleSync(): void {
  const [title] = usePageTitle();

  useEffect(() => {
    document.title = title || 'Officer';
  }, [title]);
}
