import {contrastColor} from '../utils/color.ts';
import {createSortable} from '../modules/sortable.ts';
import {GET, POST, request} from '../modules/fetch.ts';
import {hideFomanticModal} from '../modules/fomantic/modal.ts';
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
import type {SortableEvent} from 'sortablejs';
import {toggleFullScreen} from '../utils.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
import {localUserSettings} from '../modules/user-settings.ts';
import {UserEventsSharedWorker} from '../modules/worker.ts';
import {showInfoToast, showWarningToast} from '../modules/toast.ts';

// issueRef returns a short human label for a card, preferring the
// rendered "#index" anchor text and falling back to the internal id.
function issueRef(card: HTMLElement | null, issueID: number): string {
  const idx = card?.querySelector('.issue-card-title, .ref-issue, a[href*="/issues/"]')?.textContent?.trim();
  const m = idx?.match(/#\d+/);
  return m ? m[0] : `#${issueID}`;
}

function columnName(board: HTMLElement, columnID: number): string {
  const t = board.querySelector<HTMLElement>(`.project-column[data-id="${columnID}"] .project-column-title-text`)?.textContent?.trim();
  return t || `column ${columnID}`;
}

const SESSION_TAG_HEADER = 'X-Session-Tag';

// sessionTag is generated once per page load. It is attached as the
// X-Session-Tag header on every mutation request and compared against
// incoming SSE payloads so the originating tab can suppress its own
// echo (the source-of-truth DOM update already happened locally).
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;
}

function withSessionTag(headers: HeadersInit | undefined): Headers {
  const h = new Headers(headers ?? {});
  h.set(SESSION_TAG_HEADER, ensureSessionTag());
  return h;
}

function updateIssueCount(card: HTMLElement): void {
  const parent = card.parentElement!;
  const count = parent.querySelectorAll('.issue-card').length;
  parent.querySelector('.project-column-issue-count')!.textContent = String(count);
}

async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<void> {
  const columnCards = to.querySelectorAll('.issue-card');
  updateIssueCount(from);
  updateIssueCount(to);

  const columnSorting = {
    issues: Array.from(columnCards, (card, i) => ({
      issueID: parseInt(card.getAttribute('data-issue')!),
      sorting: i,
    })),
  };

  try {
    await POST(`${to.getAttribute('data-url')}/move`, {
      data: columnSorting,
      headers: withSessionTag(undefined),
    });
  } catch (error) {
    console.error(error);
    if (oldIndex !== undefined) {
      from.insertBefore(item, from.children[oldIndex]);
    }
  }
}

async function initRepoProjectSortable(): Promise<void> {
  // the HTML layout is: #project-board.board > .project-column .cards > .issue-card
  const mainBoard = document.querySelector<HTMLElement>('#project-board')!;
  let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
  createSortable(mainBoard, {
    group: 'project-column',
    draggable: '.project-column',
    handle: '.project-column-header',
    delayOnTouchOnly: true,
    delay: 500,
    onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
      boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');

      const columnSorting = {
        columns: Array.from(boardColumns, (column, i) => ({
          columnID: parseInt(column.getAttribute('data-id')!),
          sorting: i,
        })),
      };

      try {
        await POST(mainBoard.getAttribute('data-url')!, {
          data: columnSorting,
          headers: withSessionTag(undefined),
        });
      } catch (error) {
        console.error(error);
      }
    },
  });

  for (const boardColumn of boardColumns) {
    const boardCardList = boardColumn.querySelector<HTMLElement>('.cards')!;
    createSortable(boardCardList, {
      group: 'shared',
      onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
      onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
      delayOnTouchOnly: true,
      delay: 500,
    });
  }
}

function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
  const elModal = document.querySelector<HTMLElement>('.ui.modal#project-column-modal-edit')!;
  const elForm = elModal.querySelector<HTMLFormElement>('form')!;

  const elColumnId = elForm.querySelector<HTMLInputElement>('input[name="id"]')!;
  const elColumnTitle = elForm.querySelector<HTMLInputElement>('input[name="title"]')!;
  const elColumnColor = elForm.querySelector<HTMLInputElement>('input[name="color"]')!;

  const attrDataColumnId = 'data-modal-project-column-id';
  const attrDataColumnTitle = 'data-modal-project-column-title-input';
  const attrDataColumnColor = 'data-modal-project-column-color-input';

  // the "new" button is not in project board, so need to query from document
  queryElems(document, '.show-project-column-modal-edit', (el) => {
    el.addEventListener('click', () => {
      elColumnId.value = el.getAttribute(attrDataColumnId)!;
      elColumnTitle.value = el.getAttribute(attrDataColumnTitle)!;
      elColumnColor.value = el.getAttribute(attrDataColumnColor)!;
      elColumnColor.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker
    });
  });

  elForm.addEventListener('submit', async (e) => {
    e.preventDefault();
    const columnId = elColumnId.value;
    const actionBaseLink = elForm.getAttribute('data-action-base-link');

    const formData = new FormData(elForm);
    const formLink = columnId ? `${actionBaseLink}/${columnId}` : `${actionBaseLink}/columns/new`;
    const formMethod = columnId ? 'PUT' : 'POST';

    try {
      elForm.classList.add('is-loading');
      await request(formLink, {method: formMethod, data: formData, headers: withSessionTag(undefined)});
      if (!columnId) {
        window.location.reload(); // newly added column, need to reload the page
        return;
      }

      // update the newly saved column title and color in the project board (to avoid reload)
      const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`)!;
      elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value);
      elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value);

      const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${columnId}"]`)!;
      const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`)!;
      elBoardColumnTitle.textContent = elColumnTitle.value;
      if (elColumnColor.value) {
        const textColor = contrastColor(elColumnColor.value);
        elBoardColumn.style.setProperty('background', elColumnColor.value, 'important');
        elBoardColumn.style.setProperty('color', textColor, 'important');
        queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.color = textColor);
      } else {
        elBoardColumn.style.removeProperty('background');
        elBoardColumn.style.removeProperty('color');
        queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color'));
      }

      hideFomanticModal(elModal);
    } finally {
      elForm.classList.remove('is-loading');
    }
  });
}

function initRepoProjectToggleFullScreen(elProjectsView: HTMLElement): void {
  const enterFullscreenBtn = document.querySelector('.screen-full');
  const exitFullscreenBtn = document.querySelector('.screen-normal');
  if (!enterFullscreenBtn || !exitFullscreenBtn) return;

  const settingKey = 'projects-view-options';
  type ProjectsViewOptions = {
    fullScreen: boolean;
  };
  const opts = localUserSettings.getJsonObject<ProjectsViewOptions>(settingKey, {fullScreen: false});
  const toggleFullscreenState = (isFullScreen: boolean) => {
    toggleFullScreen(elProjectsView, isFullScreen);
    toggleElem(enterFullscreenBtn, !isFullScreen);
    toggleElem(exitFullscreenBtn, isFullScreen);

    opts.fullScreen = isFullScreen;
    localUserSettings.setJsonObject(settingKey, opts);
  };

  enterFullscreenBtn.addEventListener('click', () => toggleFullscreenState(true));
  exitFullscreenBtn.addEventListener('click', () => toggleFullscreenState(false));
  if (opts.fullScreen) {
    // a temporary solution to remember the full screen state, not perfect,
    // just make UX better than before, especially for users who need to change the label filter frequently and want to keep full screen mode.
    toggleFullscreenState(true);
  }
}

// SSE handlers ---------------------------------------------------------------

type EventPayloadBase = {session_tag?: string};

type CardMovedPayload = EventPayloadBase & {
  project_id: number;
  issue_id: number;
  from_column_id: number;
  to_column_id: number;
  sorting: number;
};

type CardLinkedPayload = EventPayloadBase & {
  project_id: number;
  issue_id: number;
  column_id: number;
};

type CardUnlinkedPayload = EventPayloadBase & {
  project_id: number;
  issue_id: number;
};

type ColumnUpdatedPayload = {
  project_id: number;
  column_id: number;
  title: string;
  color: string;
  sorting: number;
};

type ColumnDeletedPayload = {
  project_id: number;
  column_id: number;
};

type ColumnReorderedPayload = {
  project_id: number;
  columns: Array<{column_id: number; sorting: number}>;
};

type ProjectUpdatedPayload = {
  project_id: number;
  title: string;
  description: string;
  card_type: string;
  is_closed: boolean;
};

// columnIssuesURL builds the appropriate "list issues for column" API
// path for the current page scope. Server-side these endpoints all
// return the same JSON shape; the frontend just needs the right base.
function columnIssuesURL(board: HTMLElement, columnID: number): string | null {
  const projectID = board.getAttribute('data-project-id');
  const scope = board.getAttribute('data-project-scope');
  const owner = board.getAttribute('data-project-owner');
  const repo = board.getAttribute('data-project-repo');
  const {appSubUrl} = window.config;
  if (!projectID || !owner) return null;
  if (scope === 'repo' && repo) {
    return `${appSubUrl}/api/v1/repos/${owner}/${repo}/projects/${projectID}/columns/${columnID}/issues`;
  }
  if (scope === 'org') {
    return `${appSubUrl}/api/v1/orgs/${owner}/projects/${projectID}/columns/${columnID}/issues`;
  }
  return `${appSubUrl}/api/v1/users/${owner}/projects/${projectID}/columns/${columnID}/issues`;
}

function updateColumnCount(columnEl: HTMLElement): void {
  const cards = columnEl.querySelectorAll('.issue-card').length;
  const badge = columnEl.querySelector('.project-column-issue-count');
  if (badge) badge.textContent = String(cards);
}

function handleCardMoved(board: HTMLElement, payload: CardMovedPayload): void {
  if (payload.session_tag && payload.session_tag === sessionTag) return;
  const card = board.querySelector<HTMLElement>(`.issue-card[data-issue="${payload.issue_id}"]`);
  if (!card) {
    // Card is not currently rendered (filtered out, or new since
    // page load). A targeted column re-fetch is the safe fallback.
    refetchColumn(board, payload.to_column_id);
    showInfoToast(`#${payload.issue_id} → ${columnName(board, payload.to_column_id)}`);
    return;
  }
  const ref = issueRef(card, payload.issue_id);
  const target = board.querySelector<HTMLElement>(`#board_${payload.to_column_id}`);
  if (!target) return;
  const fromColumn = card.parentElement;
  target.append(card);
  if (fromColumn instanceof HTMLElement) {
    const fromColumnEl = fromColumn.closest<HTMLElement>('.project-column');
    if (fromColumnEl) updateColumnCount(fromColumnEl);
  }
  const toColumnEl = target.closest<HTMLElement>('.project-column');
  if (toColumnEl) updateColumnCount(toColumnEl);
  showInfoToast(`${ref} → ${columnName(board, payload.to_column_id)}`);
}

async function refetchColumn(board: HTMLElement, columnID: number): Promise<void> {
  const url = columnIssuesURL(board, columnID);
  if (!url) return;
  try {
    const resp = await GET(url);
    if (!resp.ok) return;
    // Response shape: list of API issues; we don't have a templated
    // card render available client-side, so we just refresh the
    // column count badge here. The DOM-level reorder/insert is
    // delivered by the matching CardMoved/CardUnlinked events.
    const issues = await resp.json();
    const target = board.querySelector<HTMLElement>(`#board_${columnID}`);
    if (!target) return;
    const colEl = target.closest<HTMLElement>('.project-column');
    if (colEl) {
      const badge = colEl.querySelector('.project-column-issue-count');
      if (badge) badge.textContent = String(Array.isArray(issues) ? issues.length : 0);
    }
  } catch (error) {
    console.error(error);
  }
}

function handleCardLinked(board: HTMLElement, payload: CardLinkedPayload): void {
  if (payload.session_tag && payload.session_tag === sessionTag) return;
  refetchColumn(board, payload.column_id); // no await
  showInfoToast(`#${payload.issue_id} added to ${columnName(board, payload.column_id)}`);
}

function handleCardUnlinked(board: HTMLElement, payload: CardUnlinkedPayload): void {
  if (payload.session_tag && payload.session_tag === sessionTag) return;
  const card = board.querySelector<HTMLElement>(`.issue-card[data-issue="${payload.issue_id}"]`);
  if (!card) return;
  const ref = issueRef(card, payload.issue_id);
  const colEl = card.closest<HTMLElement>('.project-column');
  card.remove();
  if (colEl) updateColumnCount(colEl);
  showInfoToast(`${ref} removed from board`);
}

function handleColumnCreated(): void {
  // Rare event; reload is cheap and avoids client-side template duplication.
  window.location.reload();
}

function handleColumnUpdated(board: HTMLElement, payload: ColumnUpdatedPayload): void {
  const colEl = board.querySelector<HTMLElement>(`.project-column[data-id="${payload.column_id}"]`);
  if (!colEl) return;
  const titleEl = colEl.querySelector<HTMLElement>('.project-column-title-text');
  const oldTitle = titleEl?.textContent?.trim();
  if (titleEl) titleEl.textContent = payload.title;
  if (oldTitle && oldTitle !== payload.title) {
    showInfoToast(`Column renamed to “${payload.title}”`);
  }
  if (payload.color) {
    const textColor = contrastColor(payload.color);
    colEl.style.setProperty('background', payload.color, 'important');
    colEl.style.setProperty('color', textColor, 'important');
    queryElemChildren(colEl, '.divider', (divider: HTMLElement) => divider.style.color = textColor);
  } else {
    colEl.style.removeProperty('background');
    colEl.style.removeProperty('color');
    queryElemChildren(colEl, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color'));
  }
}

function handleColumnDeleted(board: HTMLElement, payload: ColumnDeletedPayload): void {
  const colEl = board.querySelector<HTMLElement>(`.project-column[data-id="${payload.column_id}"]`);
  if (!colEl) return;
  const name = colEl.querySelector<HTMLElement>('.project-column-title-text')?.textContent?.trim();
  colEl.remove();
  showInfoToast(name ? `Column “${name}” removed` : 'A column was removed');
}

function handleColumnReordered(board: HTMLElement, payload: ColumnReorderedPayload): void {
  // Sort the columns array by the new sorting value, then re-attach
  // each column element in that order. appendChild on an existing
  // node moves it rather than cloning, so the result is an in-place
  // reorder.
  const order = Array.from(payload.columns).sort((a, b) => a.sorting - b.sorting);
  for (const entry of order) {
    const el = board.querySelector<HTMLElement>(`.project-column[data-id="${entry.column_id}"]`);
    if (el) board.append(el);
  }
}

function handleProjectUpdated(payload: ProjectUpdatedPayload): void {
  const header = document.querySelector<HTMLElement>('.project-header h2');
  if (header) header.textContent = payload.title;
  const desc = document.querySelector<HTMLElement>('.project-description .render-content');
  if (desc) desc.textContent = payload.description;
}

function handleProjectDeleted(): void {
  // Best-effort: navigate up one path segment from the current URL.
  // The board lives at .../projects/{id}; the listing page is the
  // parent. Falling back to the homepage on any URL we don't
  // recognise is acceptable since this is a destructive event.
  // Show a sticky warning first and delay the redirect briefly so the
  // user understands why the page is about to change under them.
  showWarningToast('This project was deleted — returning to the project list');
  const parts = window.location.pathname.split('/');
  const dest = parts.length > 1 ? (parts.slice(0, -1).join('/') || '/') : '/';
  setTimeout(() => { window.location.href = dest }, 1500);
}

// dispatchProjectEvent picks the right handler for an SSE payload.
// The backend uses one event name per project but disambiguates event
// types by payload shape; we sniff discriminating fields here. Order
// matters: the more specific shapes are checked first.
function dispatchProjectEvent(board: HTMLElement, payload: any): void {
  if ('from_column_id' in payload && 'to_column_id' in payload) {
    handleCardMoved(board, payload as CardMovedPayload);
  } else if ('column_id' in payload && 'issue_id' in payload && 'project_id' in payload) {
    handleCardLinked(board, payload as CardLinkedPayload);
  } else if ('issue_id' in payload && !('column_id' in payload)) {
    handleCardUnlinked(board, payload as CardUnlinkedPayload);
  } else if ('columns' in payload) {
    handleColumnReordered(board, payload as ColumnReorderedPayload);
  } else if ('column_id' in payload && 'title' in payload) {
    handleColumnUpdated(board, payload as ColumnUpdatedPayload);
    if ('is_default' in payload) handleColumnCreated();
  } else if ('column_id' in payload) {
    handleColumnDeleted(board, payload as ColumnDeletedPayload);
  } else if ('title' in payload && 'card_type' in payload) {
    handleProjectUpdated(payload as ProjectUpdatedPayload);
  } else if ('project_id' in payload && Object.keys(payload).length <= 2) {
    handleProjectDeleted();
  }
}

function initRepoProjectSSE(elProjectsView: HTMLElement): void {
  const board = elProjectsView.querySelector<HTMLElement>('#project-board');
  if (!board) return;
  const projectID = board.getAttribute('data-project-id');
  if (!projectID) return;
  if (!window.EventSource || !window.SharedWorker) return;

  ensureSessionTag();

  const eventName = `project-board.${projectID}`;
  let worker: UserEventsSharedWorker;
  try {
    worker = new UserEventsSharedWorker('project-board-worker');
  } catch (error) {
    console.error('project board SSE: failed to start worker', error);
    return;
  }

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

  // Subscribe to the per-project event name on top of the worker's
  // default listener set so the SharedWorker forwards us the events.
  worker.sharedWorker.port.postMessage({type: 'listen', eventType: eventName});
}

export function initRepoProjectsView(): void {
  registerGlobalInitFunc('initRepoProjectsView', (elProjectsView) => {
    initRepoProjectToggleFullScreen(elProjectsView);
    initRepoProjectSSE(elProjectsView);

    const writableProjectBoard = document.querySelector('#project-board[data-project-board-writable="true"]');
    if (!writableProjectBoard) return;

    initRepoProjectSortable(); // no await
    initRepoProjectColumnEdit(writableProjectBoard);
  });
}
