import {UserEventsSharedWorker} from '../modules/worker.ts';
import {showInfoToast, showWarningToast} from '../modules/toast.ts';

// milestoneTitle does a best-effort lookup of the milestone's display
// name for toast text. List cards expose it as a heading/link inside the
// card; the single-milestone view puts it in the page header. Falls back
// to a generic label so a toast still fires if the markup shifts.
function milestoneTitle(milestoneID: number): string {
  const card = document.querySelector<HTMLElement>(`li.milestone-card[data-milestone-id="${milestoneID}"]`);
  const fromCard = card?.querySelector('.milestone-card-title, h3, .title, a[href*="/milestone/"]')?.textContent?.trim();
  if (fromCard) return fromCard;
  const onSingle = document.querySelector<HTMLElement>(`progress[data-milestone-id="${milestoneID}"]`);
  if (onSingle) {
    const h = document.querySelector('.repository.milestone-issue-list .milestone-title, .page-content .milestone-title, h1, h2')?.textContent?.trim();
    if (h) return h;
  }
  return 'Milestone';
}

// sessionTag is generated once per page load. The mutation requests on
// milestone pages (close/open/delete/edit) flow through the existing
// fetch helpers which attach the X-Session-Tag header; the backend
// echoes it back inside SSE payloads so the originating tab can suppress
// its own echo. We only need the read side here: skip any event whose
// session_tag matches ours.
let sessionTag = '';

function ensureSessionTag(): string {
  if (sessionTag) return sessionTag;
  if (globalThis.crypto?.randomUUID) {
    sessionTag = globalThis.crypto.randomUUID();
  } else {
    sessionTag = `st-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
  }
  return sessionTag;
}

type MilestoneProgressPayload = {
  repo_id: number;
  milestone_id: number;
  open_issues: number;
  closed_issues: number;
  completeness: number;
  session_tag?: string;
};

type MilestoneDeletedPayload = {
  repo_id: number;
  milestone_id: number;
};

function isProgressPayload(p: any): p is MilestoneProgressPayload {
  return p && typeof p.completeness === 'number' && 'open_issues' in p;
}

// patchMilestoneCard updates every progress-bar / counter site for a
// single milestone id, covering both the list-card layout (milestones
// list, dashboard) and the single-milestone big progress bar.
function patchMilestoneCard(payload: MilestoneProgressPayload): void {
  const selector = `[data-milestone-id="${payload.milestone_id}"]`;
  for (const el of document.querySelectorAll<HTMLElement>(selector)) {
    // The element itself may be a <progress> (single view) or a card
    // <li> containing a <progress> (list views).
    const progressEls = el instanceof HTMLProgressElement
      ? [el]
      : Array.from(el.querySelectorAll<HTMLProgressElement>('progress'));
    for (const pe of progressEls) {
      pe.value = payload.completeness;
    }

    const scope: ParentNode = el instanceof HTMLProgressElement ? document : el;

    for (const pct of scope.querySelectorAll<HTMLElement>('.milestone-completeness-pct')) {
      // The list cards render just the number; the single-milestone
      // view renders an i18n HTML fragment ("<strong>N%</strong>
      // Completed"). Detect which by whether the node already holds a
      // <strong> child.
      const strong = pct.querySelector('strong');
      if (strong) {
        strong.textContent = `${payload.completeness}%`;
      } else {
        pct.textContent = String(payload.completeness);
      }
    }
    for (const oc of scope.querySelectorAll<HTMLElement>('.milestone-open-count')) {
      oc.textContent = String(payload.open_issues);
    }
    for (const cc of scope.querySelectorAll<HTMLElement>('.milestone-closed-count')) {
      cc.textContent = String(payload.closed_issues);
    }
  }
}

function handleMilestoneDeleted(payload: MilestoneDeletedPayload): void {
  const card = document.querySelector<HTMLElement>(`li.milestone-card[data-milestone-id="${payload.milestone_id}"]`);
  if (card) {
    card.remove();
    return;
  }
  // Single-milestone view: the milestone we are looking at is gone.
  const single = document.querySelector<HTMLElement>(`progress[data-milestone-id="${payload.milestone_id}"]`);
  if (single) {
    const parts = window.location.pathname.split('/');
    // .../milestone/{id} -> go up to the milestones listing.
    const idx = parts.lastIndexOf('milestone');
    const dest = idx > 0 ? `${parts.slice(0, idx).join('/')}/milestones` : '/';
    // Delay so the "milestone deleted" warning toast is visible before
    // the page navigates out from under the viewer.
    setTimeout(() => { window.location.href = dest }, 1500);
  }
}

function dispatchMilestoneEvent(payload: any): void {
  if (payload.session_tag && payload.session_tag === sessionTag) return;
  if (isProgressPayload(payload)) {
    patchMilestoneCard(payload);
    const total = payload.open_issues + payload.closed_issues;
    showInfoToast(`${milestoneTitle(payload.milestone_id)} · ${payload.closed_issues}/${total} closed (${payload.completeness}%)`);
  } else if ('milestone_id' in payload && 'repo_id' in payload) {
    const title = milestoneTitle(payload.milestone_id);
    handleMilestoneDeleted(payload as MilestoneDeletedPayload);
    showWarningToast(title === 'Milestone' ? 'A milestone was deleted' : `Milestone “${title}” was deleted`);
  }
}

// subscribed guards against a double subscription if more than one init
// entry point matches the same page (e.g. the dashboard milestones page
// is wired both from repo-legacy and dashboard).
let subscribed = false;

// subscribeRepos opens one SharedWorker subscription per distinct repo
// id and dispatches every "repo-milestones.{repoID}" event by payload.
function subscribeRepos(repoIDs: Set<string>): void {
  if (subscribed) return;
  if (!repoIDs.size) return;
  if (!window.EventSource || !window.SharedWorker) return;
  subscribed = true;

  ensureSessionTag();

  let worker: UserEventsSharedWorker;
  try {
    worker = new UserEventsSharedWorker('repo-milestone-worker');
  } catch (error) {
    console.error('milestone SSE: failed to start worker', error);
    return;
  }

  const eventNames = new Set<string>();
  for (const repoID of repoIDs) {
    eventNames.add(`repo-milestones.${repoID}`);
  }

  worker.addMessageEventListener((event: MessageEvent) => {
    if (!event.data || !eventNames.has(event.data.type)) return;
    let payload: any;
    try {
      payload = JSON.parse(event.data.data);
    } catch (error) {
      console.error('milestone SSE: malformed payload', error, event.data);
      return;
    }
    dispatchMilestoneEvent(payload);
  });
  worker.startPort();

  for (const name of eventNames) {
    worker.sharedWorker.port.postMessage({type: 'listen', eventType: name});
  }
}

// initRepoMilestoneListSSE wires the milestone list page and the
// dashboard milestones page: collect every distinct data-repo-id present
// on the cards (the dashboard mixes many repos) and subscribe to each.
export function initRepoMilestoneListSSE(): void {
  const cards = document.querySelectorAll<HTMLElement>('li.milestone-card[data-repo-id]');
  if (!cards.length) return;
  const repoIDs = new Set<string>();
  for (const card of cards) {
    const id = card.getAttribute('data-repo-id');
    if (id) repoIDs.add(id);
  }
  subscribeRepos(repoIDs);
}

// initRepoMilestoneSingleSSE wires the single-milestone issue list view.
export function initRepoMilestoneSingleSSE(): void {
  const progress = document.querySelector<HTMLElement>('progress[data-milestone-id][data-repo-id]');
  if (!progress) return;
  const repoID = progress.getAttribute('data-repo-id');
  if (!repoID) return;
  subscribeRepos(new Set([repoID]));
}
