"use client";

import {
  useEffect,
  useMemo,
  useState,
} from "react";

import {
  getSidebarNavItemsByPathname,
  type NavItem,
} from "@/constants/sidebar-menu";

import {
  getSidebarTranslations,
  type SidebarTranslation,
  type SidebarTranslationModule,
} from "@/constants/service/sidebar-translation-service";

import {
  isAdminRole,
  isCdcGpsfRole,
  isCdcRole,
  isCefpRole,
  isMinistryRole,
  type AuthPermission,
  type UserRole,
} from "@/features/auth/auth-data";

import type {
  LanguageCode,
} from "@/components/layout/header/header-data";

type ApiState = {
  moduleKey:
    | SidebarTranslationModule
    | null;
  items: SidebarTranslation[];
};

type TranslationNode = {
  item: SidebarTranslation;
  children: TranslationNode[];
};

function getSidebarModule(
  pathname: string,
  role: UserRole | null,
):
  | SidebarTranslationModule
  | null {
  // Admin sidebar stays static.
  if (
    pathname === "/admin" ||
    pathname.startsWith("/admin/") ||
    isAdminRole(role)
  ) {
    return null;
  }

  if (pathname.startsWith("/ministry")) {
    return "ministry";
  }

  if (pathname.startsWith("/pswg")) {
    return "pswg";
  }

  if (pathname.startsWith("/cdc-gpsf")) {
    return "cdcgpsf";
  }

  if (pathname.startsWith("/cdc")) {
    return "cdc";
  }

  if (pathname.startsWith("/cefp")) {
    return "cefp";
  }

  if (isMinistryRole(role)) {
    return "ministry";
  }

  if (isCdcGpsfRole(role)) {
    return "cdcgpsf";
  }

  if (isCdcRole(role)) {
    return "cdc";
  }

  if (isCefpRole(role)) {
    return "cefp";
  }

  return "pswg";
}

function normalizeText(
  value: string | null | undefined,
): string {
  return (value ?? "")
    .trim()
    .toLowerCase()
    .replace(/[_-]+/g, " ")
    .replace(/\s+/g, " ");
}

/**
 * Converts a static route into the old/current Translation page slug.
 *
 * Example:
 * /cdc-gpsf/meeting-requests -> meeting_requests
 * /cdc-gpsf/plenary/dashboard -> plenary_dashboard
 *
 * This is only the FIRST matching strategy.
 * If admin later changes the Translation Page slug, the hierarchy/order
 * fallback below can still connect the API record to the same sidebar item.
 */
function getExpectedPageFromUrl(
  item: NavItem,
  moduleKey: SidebarTranslationModule,
): string | null {
  if (!item.url) {
    return null;
  }

  const segments =
    item.url
      .split("/")
      .filter(Boolean);

  if (segments.length < 2) {
    return null;
  }

  const routeModule = segments[0];

  const expectedModule =
    moduleKey === "cdcgpsf"
      ? "cdc-gpsf"
      : moduleKey;

  if (routeModule !== expectedModule) {
    return null;
  }

  const routeSegments =
    segments.slice(1);

  // CDC/CEFP dashboard parent currently points to dashboard/plenary.
  if (
    item.titleEn === "Dashboard" &&
    item.items.length > 0 &&
    routeSegments.join("/") === "dashboard/plenary"
  ) {
    return "dashboard";
  }

  return routeSegments
    .map((segment) =>
      segment.replace(/-/g, "_"),
    )
    .join("_");
}

function getPreferredTitleRecords(
  translations: SidebarTranslation[],
): SidebarTranslation[] {
  const byPage =
    new Map<
      string,
      SidebarTranslation[]
    >();

  for (const item of translations) {
    if (!item.page) {
      continue;
    }

    const key =
      `${item.parentId ?? "root"}::${item.page}`;

    const list =
      byPage.get(key) ?? [];

    list.push(item);
    byPage.set(key, list);
  }

  const result: SidebarTranslation[] = [];

  for (const records of byPage.values()) {
    const title =
      records.find(
        (record) =>
          record.translationKey === "title",
      ) ??
      records[0];

    if (title) {
      result.push(title);
    }
  }

  return result.sort(
    (a, b) => a.id - b.id,
  );
}

function buildTranslationTree(
  translations: SidebarTranslation[],
): TranslationNode[] {
  const titleRecords =
    getPreferredTitleRecords(
      translations,
    );

  const nodeById =
    new Map<
      number,
      TranslationNode
    >();

  for (const item of titleRecords) {
    nodeById.set(item.id, {
      item,
      children: [],
    });
  }

  const roots: TranslationNode[] = [];

  for (const node of nodeById.values()) {
    const parentId =
      node.item.parentId ?? null;

    if (
      parentId !== null &&
      nodeById.has(parentId)
    ) {
      nodeById
        .get(parentId)!
        .children.push(node);
    } else {
      roots.push(node);
    }
  }

  for (const node of nodeById.values()) {
    node.children.sort(
      (a, b) =>
        a.item.id - b.item.id,
    );
  }

  roots.sort(
    (a, b) =>
      a.item.id - b.item.id,
  );

  return roots;
}

function findExactNode(
  staticItem: NavItem,
  candidates: TranslationNode[],
  moduleKey: SidebarTranslationModule,
): TranslationNode | null {
  const expectedPage =
    getExpectedPageFromUrl(
      staticItem,
      moduleKey,
    );

  if (expectedPage) {
    const byPage =
      candidates.find(
        (node) =>
          node.item.page ===
          expectedPage,
      );

    if (byPage) {
      return byPage;
    }
  }

  // Secondary match. Useful before the admin renames the label.
  const staticEnglish =
    normalizeText(
      staticItem.titleEn,
    );

  const byEnglish =
    candidates.find(
      (node) =>
        normalizeText(
          node.item.englishText,
        ) === staticEnglish,
    );

  if (byEnglish) {
    return byEnglish;
  }

  return null;
}

/**
 * Parent pages such as WG / Plenary can now use ANY Page slug and ANY label.
 *
 * We identify a parent primarily from its children:
 * - static WG contains routes dashboard, meeting_requests, ...
 * - API parent contains child Translation rows with those page keys
 *
 * Therefore changing:
 *   page = "wg"
 * to:
 *   page = "working_group_section"
 *
 * and changing:
 *   WG -> Working Group Test
 *
 * still works.
 */
function scoreParentCandidate(
  staticItem: NavItem,
  candidate: TranslationNode,
  moduleKey: SidebarTranslationModule,
): number {
  if (staticItem.items.length === 0) {
    return 0;
  }

  const expectedChildPages =
    new Set(
      staticItem.items
        .map((child) =>
          getExpectedPageFromUrl(
            child,
            moduleKey,
          ),
        )
        .filter(
          (
            page,
          ): page is string =>
            Boolean(page),
        ),
    );

  if (
    expectedChildPages.size ===
    0
  ) {
    return 0;
  }

  let score = 0;

  for (const child of candidate.children) {
    if (
      child.item.page &&
      expectedChildPages.has(
        child.item.page,
      )
    ) {
      score += 1;
    }
  }

  return score;
}

function pickRootNode(
  staticItem: NavItem,
  staticIndex: number,
  candidates: TranslationNode[],
  usedIds: Set<number>,
  moduleKey: SidebarTranslationModule,
): TranslationNode | null {
  const available =
    candidates.filter(
      (node) =>
        !usedIds.has(node.item.id),
    );

  if (available.length === 0) {
    return null;
  }

  // 1. Exact route/page/title match.
  const exact =
    findExactNode(
      staticItem,
      available,
      moduleKey,
    );

  if (exact) {
    return exact;
  }

  // 2. Parent hierarchy match.
  if (staticItem.items.length > 0) {
    const parentCandidates =
      available.filter(
        (node) =>
          node.children.length > 0,
      );

    let best:
      | TranslationNode
      | null = null;
    let bestScore = 0;

    for (const candidate of parentCandidates) {
      const score =
        scoreParentCandidate(
          staticItem,
          candidate,
          moduleKey,
        );

      if (score > bestScore) {
        best = candidate;
        bestScore = score;
      }
    }

    if (best && bestScore > 0) {
      return best;
    }

    // 3. Fully dynamic parent fallback:
    // same hierarchical position among API parent groups.
    const staticParentPosition =
      staticIndex;

    if (
      parentCandidates[
        staticParentPosition
      ]
    ) {
      return parentCandidates[
        staticParentPosition
      ];
    }

    if (parentCandidates[0]) {
      return parentCandidates[0];
    }
  }

  // 4. Standalone/root leaf fallback.
  if (staticItem.items.length === 0) {
    const leafCandidates =
      available.filter(
        (node) =>
          node.children.length === 0,
      );

    if (leafCandidates.length > 0) {
      return leafCandidates[0];
    }
  }

  return null;
}

function matchChildNodes(
  staticChildren: NavItem[],
  parentNode: TranslationNode,
  moduleKey: SidebarTranslationModule,
): Array<TranslationNode | null> {
  const result: Array<TranslationNode | null> =
    staticChildren.map(() => null);

  const usedIds = new Set<number>();

  /*
   * PASS 1: reserve every exact route/page/title match FIRST.
   *
   * This is important when one API child is inactive/missing.
   *
   * Example:
   * static children:
   *   dashboard_plenary
   *   dashboard_working_group
   *
   * active API children:
   *   dashboard_working_group
   *
   * We MUST reserve dashboard_working_group for the second static item.
   * Otherwise position fallback would wrongly use it for Plenary and the
   * sidebar would show:
   *   Working Group
   *   Working Group
   */
  staticChildren.forEach(
    (staticChild, index) => {
      const available =
        parentNode.children.filter(
          (node) =>
            !usedIds.has(
              node.item.id,
            ),
        );

      const exact =
        findExactNode(
          staticChild,
          available,
          moduleKey,
        );

      if (!exact) {
        return;
      }

      result[index] = exact;
      usedIds.add(
        exact.item.id,
      );
    },
  );

  /*
   * PASS 2: only unmatched static children can use the remaining API
   * children as the fully-dynamic fallback.
   *
   * This preserves the earlier requirement that an admin may rename both
   * Page + label, while preventing a missing/inactive child from stealing
   * another child's exact API record.
   */
  const remainingApiChildren =
    parentNode.children.filter(
      (node) =>
        !usedIds.has(
          node.item.id,
        ),
    );

  let remainingIndex = 0;

  for (
    let index = 0;
    index <
    staticChildren.length;
    index += 1
  ) {
    if (result[index]) {
      continue;
    }

    const fallbackNode =
      remainingApiChildren[
        remainingIndex
      ];

    if (!fallbackNode) {
      /*
       * No API child available:
       * return null so withApiLabel() keeps the STATIC label.
       */
      continue;
    }

    result[index] =
      fallbackNode;

    usedIds.add(
      fallbackNode.item.id,
    );

    remainingIndex += 1;
  }

  return result;
}

function withApiLabel(
  staticItem: NavItem,
  apiNode: TranslationNode | null,
  language: LanguageCode,
): NavItem {
  if (!apiNode) {
    return staticItem;
  }

  const titleEn =
    apiNode.item.englishText?.trim() ||
    staticItem.titleEn;

  const titleKh =
    apiNode.item.khmerText?.trim() ||
    staticItem.titleKh;

  return {
    ...staticItem,
    titleEn,
    titleKh,
    title:
      language === "kh"
        ? titleKh
        : titleEn,
  };
}

function applyDynamicApiTree(
  staticItems: NavItem[],
  translations: SidebarTranslation[],
  moduleKey: SidebarTranslationModule,
  language: LanguageCode,
): NavItem[] {
  const roots =
    buildTranslationTree(
      translations,
    );

  const usedRootIds =
    new Set<number>();

  let parentGroupPosition = 0;

  return staticItems.map(
    (staticItem) => {
      const rootNode =
        pickRootNode(
          staticItem,
          staticItem.items.length > 0
            ? parentGroupPosition
            : 0,
          roots,
          usedRootIds,
          moduleKey,
        );

      if (
        staticItem.items.length >
        0
      ) {
        parentGroupPosition += 1;
      }

      if (rootNode) {
        usedRootIds.add(
          rootNode.item.id,
        );
      }

      const translatedParent =
        withApiLabel(
          staticItem,
          rootNode,
          language,
        );

      if (
        staticItem.items.length ===
          0 ||
        !rootNode
      ) {
        return translatedParent;
      }

      const matchedChildren =
        matchChildNodes(
          staticItem.items,
          rootNode,
          moduleKey,
        );

      const translatedChildren =
        staticItem.items.map(
          (
            staticChild,
            childIndex,
          ) =>
            withApiLabel(
              staticChild,
              matchedChildren[
                childIndex
              ] ?? null,
              language,
            ),
        );

      return {
        ...translatedParent,
        items:
          translatedChildren,
      };
    },
  );
}

export function useSidebarNavItemsByPathname(
  pathname: string,
  role: UserRole | null,
  language: LanguageCode,
  permissions: AuthPermission[] = [],
): NavItem[] {
  const staticItems =
    useMemo(
      () =>
        getSidebarNavItemsByPathname(
          pathname,
          role,
          language,
          permissions,
        ),
      [
        pathname,
        role,
        language,
        permissions,
      ],
    );

  const moduleKey =
    useMemo(
      () =>
        getSidebarModule(
          pathname,
          role,
        ),
      [pathname, role],
    );

  const [
    apiState,
    setApiState,
  ] =
    useState<ApiState>({
      moduleKey: null,
      items: [],
    });

  useEffect(() => {
    if (!moduleKey) {
      return;
    }

    let cancelled = false;

    const timer =
      window.setTimeout(
        () => {
          void (async () => {
            try {
              const items =
                await getSidebarTranslations(
                  moduleKey,
                );

              if (cancelled) {
                return;
              }

              setApiState({
                moduleKey,
                items,
              });
            } catch {
              if (cancelled) {
                return;
              }

              setApiState({
                moduleKey,
                items: [],
              });
            }
          })();
        },
        0,
      );

    return () => {
      cancelled = true;
      window.clearTimeout(
        timer,
      );
    };
  }, [moduleKey]);

  return useMemo(() => {
    // API unavailable / empty -> current static menu.
    if (
      !moduleKey ||
      apiState.moduleKey !==
        moduleKey ||
      apiState.items.length === 0
    ) {
      return staticItems;
    }

    return applyDynamicApiTree(
      staticItems,
      apiState.items,
      moduleKey,
      language,
    );
  }, [
    apiState,
    language,
    moduleKey,
    staticItems,
  ]);
}