/**
 * UserProfilePopup — anchored popover that appears when clicking a username
 * in the chat or member list. Shows avatar, username, role badge, status dot,
 * about section, join date, and Message/Call action buttons.
 *
 * Position: anchored to the click point, flipped to the other side and clamped
 * against the measured card height so it always lands fully on screen.
 * Animation: fade+scale, defined in CSS so reduced-motion can drop it.
 * Close: outside click or Escape.
 * A11y: role="dialog", aria-label, focus trap, return focus on close.
 */

import { createElement, appendChildren, setText } from "@lib/dom";
import { createIcon } from "@lib/icons";
import type { MountableComponent } from "@lib/safe-render";
import type { UserStatus } from "@lib/types";
import { createAvatarElement, resolveDisplayName } from "@lib/avatar";
import { roleColorVar } from "./message-list/formatting";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface UserProfileData {
  readonly id: number;
  readonly username: string;
  readonly avatar: string | null;
  readonly role: string;
  readonly status: UserStatus;
  /** Nickname. When set the popup shows it as the heading and the username
   *  underneath, because the username is still the handle you @mention. */
  readonly displayName?: string | null;
  readonly about?: string | null;
  /** Free-text status line, shown under the name. */
  readonly customStatus?: string | null;
  readonly joinDate?: string | null;
  readonly isDeleted?: boolean;
}

export interface UserProfilePopupOptions {
  readonly user: UserProfileData;
  /** Anchor point — the click event's clientX/clientY. */
  readonly anchorX: number;
  readonly anchorY: number;
  /** Called when the user clicks "Message". */
  readonly onMessage?: (userId: number) => void;
  /** Called when the user clicks "Call". */
  readonly onCall?: (userId: number) => void;
}

export type UserProfilePopupComponent = MountableComponent & {
  /** Check if the popup is currently visible. */
  isOpen(): boolean;
};

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const POPUP_WIDTH = 300;
/** Keeps the card clear of the window edges on both axes. */
const VIEWPORT_MARGIN = 8;
/** Breathing room between the click point and the card. */
const ANCHOR_GAP = 8;

const STATUS_COLORS: Record<UserStatus, string> = {
  online: "#3ba55d",
  idle: "#faa61a",
  dnd: "#ed4245",
  // Only ever reached for the signed-in user looking at their own profile —
  // the server maps invisible to offline for everyone else.
  invisible: "#747f8d",
  offline: "#747f8d",
};

const STATUS_LABELS: Record<UserStatus, string> = {
  online: "Online",
  idle: "Idle",
  dnd: "Do Not Disturb",
  invisible: "Invisible",
  offline: "Offline",
};

// ---------------------------------------------------------------------------
// Component factory
// ---------------------------------------------------------------------------

export function createUserProfilePopup(
  options: UserProfilePopupOptions,
): UserProfilePopupComponent {
  const ac = new AbortController();
  const { signal } = ac;

  let overlay: HTMLDivElement | null = null;
  let popup: HTMLDivElement | null = null;
  let previousFocus: Element | null = null;

  function isOpen(): boolean {
    return popup !== null && overlay !== null;
  }

  function close(): void {
    if (overlay !== null) {
      overlay.remove();
      overlay = null;
    }
    popup = null;
    ac.abort();

    // Return focus to the element that was focused before opening
    if (previousFocus instanceof HTMLElement) {
      previousFocus.focus();
    }
  }

  /**
   * Place the card beside the anchor, flipping and clamping so it always lands
   * fully on screen — Discord opens its popout away from whichever edge the
   * clicked row is nearest.
   *
   * The height is measured rather than assumed. The previous version guessed
   * 300px and only clamped the top edge, so a member clicked low in the list
   * opened a card that ran off the bottom of the window.
   */
  function position(el: HTMLElement, anchorX: number, anchorY: number): void {
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    const height = el.offsetHeight;

    // Prefer the right of the anchor and flip left when there is no room. The
    // member list sits against the right edge, so flipping is the usual case.
    let left = anchorX + ANCHOR_GAP;
    if (left + POPUP_WIDTH > vw - VIEWPORT_MARGIN) {
      left = anchorX - POPUP_WIDTH - ANCHOR_GAP;
    }
    left = Math.max(VIEWPORT_MARGIN, Math.min(left, vw - POPUP_WIDTH - VIEWPORT_MARGIN));

    // Align the top with the click, then lift the card just enough to fit.
    let top = anchorY;
    if (top + height > vh - VIEWPORT_MARGIN) {
      top = vh - height - VIEWPORT_MARGIN;
    }
    top = Math.max(VIEWPORT_MARGIN, top);

    el.style.left = `${left}px`;
    el.style.top = `${top}px`;
  }

  function buildAvatar(user: UserProfileData): HTMLDivElement {
    // The shared helper is what makes uploaded avatars work here and in the
    // message rows and member list at the same time: it fetches the
    // authenticated file through the cert-pinned path and falls back to the
    // letter until (or unless) the bytes arrive.
    const wrapper = createAvatarElement(
      {
        username: user.username,
        displayName: user.displayName,
        avatar: user.avatar,
        isDeleted: user.isDeleted,
      },
      {
        className: "upp-avatar",
        background: user.isDeleted === true ? "#4e5058" : "var(--accent, #5865f2)",
      },
    );

    // Status dot overlay
    const statusDot = createElement("div", { class: "upp-status-dot" });
    statusDot.style.background = STATUS_COLORS[user.status] ?? STATUS_COLORS.offline;
    statusDot.title = STATUS_LABELS[user.status] ?? "Offline";
    wrapper.appendChild(statusDot);

    return wrapper;
  }

  function mount(container: Element): void {
    previousFocus = document.activeElement;
    const user = options.user;
    const displayName = user.isDeleted === true ? "[deleted]" : resolveDisplayName(user);

    // Overlay for outside-click detection
    overlay = createElement("div", {
      class: "upp-overlay",
      "data-testid": "user-profile-overlay",
    });

    // Popup container
    popup = createElement("div", {
      class: "upp-popup",
      role: "dialog",
      "aria-label": "User profile",
      "aria-modal": "true",
      tabindex: "-1",
      "data-testid": "user-profile-popup",
    });

    popup.style.width = `${POPUP_WIDTH}px`;

    // --- Content ---

    // Avatar
    const avatar = buildAvatar(user);

    // Username
    const nameEl = createElement("div", { class: "upp-username" }, displayName);
    if (user.isDeleted === true) {
      nameEl.style.color = "var(--text-faint, #80848e)";
    }

    // Username line, shown only when a display name is standing in for it.
    // @mentions still resolve by username, so the popup has to keep telling
    // you what to type.
    const handleEl = createElement("div", { class: "upp-username-handle" });
    if (user.isDeleted !== true && displayName !== user.username) {
      setText(handleEl, `@${user.username}`);
    }

    // Custom status line — the user's own words, under the name.
    const customStatusEl = createElement("div", { class: "upp-custom-status" });
    if (typeof user.customStatus === "string" && user.customStatus.length > 0) {
      setText(customStatusEl, user.customStatus);
    }

    // Role badge
    const roleBadge = createElement("span", { class: "upp-role-badge" });
    const roleDot = createElement("span", { class: "upp-role-dot" });
    roleDot.style.background = roleColorVar(user.role.toLowerCase());
    const roleLabel = createElement(
      "span",
      {},
      user.role.charAt(0).toUpperCase() + user.role.slice(1),
    );
    appendChildren(roleBadge, roleDot, roleLabel);

    // Status line
    const statusLine = createElement("div", { class: "upp-status-line" });
    const statusDotInline = createElement("span", { class: "upp-status-dot-inline" });
    statusDotInline.style.background = STATUS_COLORS[user.status] ?? STATUS_COLORS.offline;
    const statusText = createElement("span", {}, STATUS_LABELS[user.status] ?? "Offline");
    appendChildren(statusLine, statusDotInline, statusText);

    // About section (2 lines max)
    const aboutSection = createElement("div", { class: "upp-about" });
    if (user.about !== undefined && user.about !== null && user.about.length > 0) {
      const aboutTitle = createElement("div", { class: "upp-section-title" }, "ABOUT ME");
      const aboutText = createElement("div", { class: "upp-about-text" }, user.about);
      appendChildren(aboutSection, aboutTitle, aboutText);
    }

    // Join date
    const joinSection = createElement("div", { class: "upp-join-date" });
    if (user.joinDate !== undefined && user.joinDate !== null) {
      const joinTitle = createElement("div", { class: "upp-section-title" }, "MEMBER SINCE");
      const joinText = createElement("div", { class: "upp-join-text" }, user.joinDate);
      appendChildren(joinSection, joinTitle, joinText);
    }

    // Divider
    const divider = createElement("div", { class: "upp-divider" });

    // Actions — only render buttons that are actually wired up, so the popup
    // never shows a dead control (e.g. Call before DM calls exist, or Message
    // on your own profile).
    const actions = createElement("div", { class: "upp-actions" });

    if (options.onMessage !== undefined) {
      const onMessage = options.onMessage;
      const messageBtn = createElement("button", {
        class: "upp-action-btn",
        "data-testid": "upp-message-btn",
      });
      messageBtn.appendChild(createIcon("send", 16));
      messageBtn.appendChild(document.createTextNode(" Message"));
      messageBtn.addEventListener(
        "click",
        () => {
          onMessage(user.id);
          close();
        },
        { signal },
      );
      actions.appendChild(messageBtn);
    }

    if (options.onCall !== undefined) {
      const onCall = options.onCall;
      const callBtn = createElement("button", {
        class: "upp-action-btn",
        "data-testid": "upp-call-btn",
      });
      callBtn.appendChild(createIcon("phone", 16));
      callBtn.appendChild(document.createTextNode(" Call"));
      callBtn.addEventListener(
        "click",
        () => {
          onCall(user.id);
          close();
        },
        { signal },
      );
      actions.appendChild(callBtn);
    }

    // Assemble the card: a banner strip and a body, with the avatar straddling
    // the seam between them the way Discord's popout does.
    const banner = createElement("div", { class: "upp-banner" });
    const body = createElement("div", { class: "upp-body" });
    appendChildren(
      body,
      nameEl,
      handleEl,
      customStatusEl,
      roleBadge,
      statusLine,
      aboutSection,
      joinSection,
    );
    if (actions.childElementCount > 0) {
      appendChildren(body, divider, actions);
    }
    // The avatar hangs off the body's top edge, so it is a child of the card
    // rather than the body — the body scrolls, and a scroll container clips.
    // Appending it last puts it over the banner without needing a z-index.
    appendChildren(popup, banner, body, avatar);

    overlay.appendChild(popup);
    container.appendChild(overlay);

    // Measure, then place: the card has to be in the document before it has a
    // height to clamp against.
    position(popup, options.anchorX, options.anchorY);

    // The fade+scale itself lives in CSS so `prefers-reduced-motion` can drop it.
    requestAnimationFrame(() => {
      if (popup !== null) {
        popup.classList.add("open");
      }
    });

    // Focus the popup for a11y
    popup.focus();

    // Close on outside click (click on overlay but not on popup)
    overlay.addEventListener(
      "mousedown",
      (e: MouseEvent) => {
        if (popup !== null && !popup.contains(e.target as Node)) {
          close();
        }
      },
      { signal },
    );

    // Close on Escape
    document.addEventListener(
      "keydown",
      (e: KeyboardEvent) => {
        if (e.key === "Escape" && isOpen()) {
          close();
        }
      },
      { signal },
    );

    // Focus trap: keep focus inside popup
    popup.addEventListener(
      "keydown",
      (e: KeyboardEvent) => {
        if (e.key !== "Tab" || popup === null) return;
        const focusable = popup.querySelectorAll<HTMLElement>(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
        );
        if (focusable.length === 0) return;

        const first = focusable[0]!;
        const last = focusable[focusable.length - 1]!;

        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      },
      { signal },
    );
  }

  function destroy(): void {
    close();
  }

  return { mount, destroy, isOpen };
}
