import React, { lazy, Suspense } from 'react';
import { Outlet, Route } from 'react-router-dom';
import { Spin } from 'antd';
import generatedResources from 'generated/resources';

// ─── Types ───────────────────────────────────────────────────────────────────

type LazyComp = React.LazyExoticComponent<React.ComponentType<any>>;
// Allow both lazy and eagerly-imported components (generated/resources uses the latter)
type AnyComp = React.ComponentType<any>;

interface RouteEntry {
  path: string;
  component?: LazyComp | AnyComp;
  wrapper?: LazyComp | AnyComp;
  index?: boolean;
  children?: Record<string, RouteEntry>;
  /**
   * Programmatic-only — never hand-written.
   * Set automatically by buildDataMngtChildren (res.name) or derived via
   * extractSourceDir() from the component's import string at index-build time.
   * Identifies the routes/ subdirectory that owns this route group.
   */
  _sourceDir?: string;
}

// ─── Data-Mngt dynamic route helpers ─────────────────────────────────────────

/**
 * Resource names already covered by the static registry entries below.
 * These must NOT be auto-registered again under dataMngt.
 */
const SYSTEM_RESOURCE_NAMES = new Set([
  'user',
  'role',
  'page',
  'resource',
  'audit',
  'game',
  'roleResource',
  'pageResource',
  'rolePages',
]);

/** camelCase → PascalCase  (reverses toCamelCase in seedDataMngtPages) */
export function toPascalCase(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
 * PascalCase → kebab-case  (same algorithm as seedDataMngtPages.ts so the
 * generated route path always matches the page.path stored in the DB).
 *   "Event"         → "event"
 *   "ITActivityDay" → "it-activity-day"
 */
function toKebabCase(str: string): string {
  return str
    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
    .toLowerCase();
}

/**
 * Build RouteEntry children for the dataMngt segment by scanning
 * generated/resources.tsx (produced by gadmin2 on every `generate:dev`).
 *
 * Each business-model resource becomes:
 *   <Route path="<slug>">
 *     <Route index          element={<List   />} />
 *     <Route path="show/:id"  element={<Show   />} />
 *     <Route path="edit/:id"  element={<Edit   />} />
 *     <Route path="create"    element={<Create />} />
 *   </Route>
 */
function buildDataMngtChildren(): Record<string, RouteEntry> {
  const children: Record<string, RouteEntry> = {};

  for (const res of generatedResources) {
    if (SYSTEM_RESOURCE_NAMES.has(res.name)) continue;

    // generated/resources stores components in meta.components
    const {
      list: ListComp,
      show: ShowComp,
      edit: EditComp,
      create: CreateComp,
    } = (res.meta as any).components ?? {};

    if (!ListComp) continue;

    // Derive the URL slug from the PascalCase model name so it matches
    // what seedDataMngtPages.ts writes into page.path.
    const slug = toKebabCase(toPascalCase(res.name));

    children[res.name] = {
      path: slug,
      _sourceDir: res.name, // directory name == resource name for dataMngt routes
      // No component on the parent — React Router renders <Outlet /> so that
      // the index / show / edit / create children can each occupy the full view.
      children: {
        list: { path: '', index: true, component: ListComp as AnyComp },
        show: { path: 'show/:id', component: ShowComp as AnyComp },
        edit: { path: 'edit/:id', component: EditComp as AnyComp },
        create: { path: 'create', component: CreateComp as AnyComp },
        clone: { path: 'clone/:id', component: CreateComp as AnyComp },
      },
    };
  }

  return children;
}

/**
 * Extract the routes/ subdirectory name from a lazy component's import string.
 *
 * Works by calling toString() on the factory function — in both dev (Vite
 * module-graph) and build (Rollup) modes the original import specifier is
 * preserved in the stringified source.
 *
 * Examples:
 *   import("routes/user")          → "user"
 *   import("routes/iTActivityDay") → "iTActivityDay"
 *   import("routes/page/list")     → "page"   (strip trailing page-type segment)
 *
 * Returns null when the string cannot be parsed (safe fallback).
 */
function extractSourceDir(
  component: LazyComp | AnyComp | undefined,
): string | null {
  if (!component) return null;
  try {
    // React.lazy stores the factory in _payload._result before the promise
    // resolves. Stringify it to find the original import() specifier.
    const c = component as any;
    const factory: unknown = c._payload?._result;
    const src =
      typeof factory === 'function'
        ? (factory as () => unknown).toString()
        : typeof component === 'function'
          ? (component as () => unknown).toString()
          : '';

    const match = src.match(/import\(["'`]routes\/([^"'`]+)["'`]\)/);
    if (!match) return null;

    // Strip trailing page-type segments: list, create, edit, show, index
    const parts = match[1].split('/');
    const PAGE_TYPES = new Set(['list', 'create', 'edit', 'show', 'index']);
    while (parts.length > 1 && PAGE_TYPES.has(parts[parts.length - 1])) {
      parts.pop();
    }
    return parts.join('/') || null;
  } catch {
    return null;
  }
}

const S = ({ children }: { children: React.ReactNode }) => (
  <Suspense fallback={<Spin size="large" />}>{children}</Suspense>
);

// ─── Route Registry ──────────────────────────────────────────────────────────
//
// 路由路径命名规则：
// Sider 组件会对不在菜单中的路径执行 auto-redirect。豁免规则（见
// components/layout/sider.tsx 中的 useEffect）：
//   ✅ 路径含 `/edit/` 或 `/show/`，或以 `/create` 结尾（CRUD 详情页）
//   ✅ 路径含 `/data-mngt/`（数据管理 CRUD 列表，部分通过 meta.hide 隐藏）
//   ❌ 其它不在菜单的列表路径会被自动重定向到第一个菜单项
// 因此，新加的不在侧边栏的子页面，路径必须命中上述某条豁免规则，例如：
//   ✅ instance/show/:id
//   ✅ node-instances/edit/:id
//   ❌ instance/:id          ← 会被 auto-redirect 导致 404
//

export const routeRegistry: Record<string, RouteEntry> = {
  // ── Administration ─────────────────────────────────────────────────────────
  admin: {
    path: '/admin',
    children: {
      permission: {
        path: 'permission',
        children: {
          // 菜单指引 — sortOrder 1020, code: admin.permission.readme
          permission_readme: {
            path: 'readme',
            component: lazy(() =>
              import('routes/permissionReadme').then((m) => ({
                default: m.PermissionReadmePage,
              })),
            ),
          },
          // 用户管理 — sortOrder 1030, code: admin.permission.user
          user: {
            path: 'user',
            component: lazy(() =>
              import('routes/user').then((m) => ({ default: m.UserListPage })),
            ),
          },
          // 角色管理 — sortOrder 1040, code: admin.permission.role
          role: {
            path: 'role-list',
            component: lazy(() =>
              import('routes/role').then((m) => ({
                default: m.RoleManagementListPage,
              })),
            ),
          },
          // 页面管理 — sortOrder 1050, code: admin.permission.page
          page: {
            path: 'page-list',
            component: lazy(() =>
              import('routes/page/list').then((m) => ({
                default: m.PageManagementListPage,
              })),
            ),
          },
          // 资源管理 — sortOrder 1060, code: admin.permission.resource
          resource: {
            path: 'resource-list',
            component: lazy(() =>
              import('routes/resource').then((m) => ({
                default: m.ResourceManagementListPage,
              })),
            ),
          },
        },
      },
      dataMngt: {
        path: 'data-mngt',
        children: buildDataMngtChildren(),
      },
      // Canvas
      canvas: {
        path: 'canvas',
        children: {
          list: {
            path: '',
            index: true,
            component: lazy(() =>
              import('routes/canvas').then((m) => ({
                default: m.CanvasListPage,
              })),
            ),
          },
          edit: {
            path: 'edit/:id',
            component: lazy(() =>
              import('routes/canvas').then((m) => ({
                default: m.CanvasEditPage,
              })),
            ),
          },
        },
      },
      // Canvas published pages (dynamic, DB-driven)
      'canvas-page': {
        path: 'p/:slug',
        component: lazy(() => import('routes/canvas-page')),
      },
      /*game: {
        path: 'game-list',
        children: {
          list: {
            path: '',
            index: true,
            component: lazy(() =>
              import('routes/game').then((m) => ({ default: m.GameList })),
            ),
          },
          show: {
            path: 'show/:id',
            component: lazy(() =>
              import('routes/game').then((m) => ({ default: m.GameShow })),
            ),
          },
          edit: {
            path: 'edit/:id',
            component: lazy(() =>
              import('routes/game').then((m) => ({ default: m.GameEdit })),
            ),
          },
          create: {
            path: 'create',
            component: lazy(() =>
              import('routes/game').then((m) => ({ default: m.GameCreate })),
            ),
          },
        },
      },*/
      // Agenda Jobs — sortOrder 1080, code: admin.agenda
      agenda: {
        path: 'agenda-jobs',
        children: {
          list: {
            path: '',
            index: true,
            component: lazy(() => import('routes/agenda')),
          },
          show: {
            path: 'show/:jobName',
            component: lazy(() => import('routes/agenda/show')),
          },
        },
      },
      // Workflow — code: admin.workflow (parent menu)
      workflow: {
        path: 'workflow',
        children: {
          workflow_list: {
            path: '',
            index: true,
            component: lazy(() => import('routes/workflow')),
          },
          editor_new: {
            path: 'create',
            component: lazy(() => import('routes/workflow/editor')),
          },
          instances: {
            path: 'instances/show/:workflowId',
            component: lazy(() => import('routes/workflow/instances')),
          },
          instanceDetail: {
            path: 'instance/show/:instanceId',
            component: lazy(() => import('routes/workflow/instance-detail')),
          },
          editor: {
            path: 'edit/:id',
            component: lazy(() => import('routes/workflow/editor')),
          },
          show: {
            path: 'show/:id',
            component: lazy(() => import('routes/workflow/show')),
          },
          workflow_node_instance: {
            path: 'node-instances',
            component: lazy(() => import('routes/workflow/node-instances')),
          },
          nodeInstanceCreate: {
            path: 'node-instances/create',
            component: lazy(
              () => import('routes/workflow/node-instances/create'),
            ),
          },
          nodeInstanceEdit: {
            path: 'node-instances/edit/:id',
            component: lazy(
              () => import('routes/workflow/node-instances/edit'),
            ),
          },
          nodeInstanceShow: {
            path: 'node-instances/show/:id',
            component: lazy(
              () => import('routes/workflow/node-instances/show'),
            ),
          },
        },
      },
      // 操作日志 — sortOrder 1070, code: admin.audit
      audits: {
        path: 'audit-log-list',
        component: lazy(() =>
          import('routes/audit').then((m) => ({ default: m.AuditLogPage })),
        ),
      },
    },
  },
};

// ─── Path + SourceDir indices ─────────────────────────────────────────────────

/**
 * Walk the registry tree once and produce two flat maps:
 *
 * pathIndex      : routeKey → absolute URL path  (e.g. "user" → "/admin/permission/user")
 * sourceDirIndex : URL pattern → routes/ dir     (e.g. "/admin/permission/user" → "user")
 *
 * URL patterns in sourceDirIndex use ":id" for dynamic segments so they can
 * be matched against live pathnames after replacing numeric/uuid segments.
 */
function buildIndices(
  entries: Record<string, RouteEntry>,
  parentPath = '',
  inheritedSourceDir: string | null = null,
): {
  pathIndex: Record<string, string>;
  sourceDirIndex: Record<string, string>;
} {
  const pathIndex: Record<string, string> = {};
  const sourceDirIndex: Record<string, string> = {};

  for (const [key, entry] of Object.entries(entries)) {
    const fullPath = entry.path.startsWith('/')
      ? entry.path
      : entry.path === ''
        ? parentPath
        : entry.path.startsWith('?')
          ? parentPath + entry.path
          : parentPath + '/' + entry.path;

    pathIndex[key] = fullPath;

    // Resolve sourceDir for this entry:
    // Priority: explicit _sourceDir > extracted from component > inherited from parent
    const resolved =
      entry._sourceDir ??
      extractSourceDir(entry.component) ??
      inheritedSourceDir;

    // Register in sourceDirIndex when we have both a real path and a sourceDir.
    // Use the path without query-string, with dynamic segments normalised to ":id".
    if (resolved) {
      const pattern = fullPath.split('?')[0];
      sourceDirIndex[pattern] = resolved;
    }

    if (entry.children) {
      const sub = buildIndices(
        entry.children,
        fullPath.split('?')[0],
        resolved,
      );
      Object.assign(pathIndex, sub.pathIndex);
      Object.assign(sourceDirIndex, sub.sourceDirIndex);
    }
  }

  return { pathIndex, sourceDirIndex };
}

const { pathIndex, sourceDirIndex } = buildIndices(routeRegistry);

export function getRoutePath(pageCode: string): string | null {
  return pathIndex[pageCode] ?? null;
}

/**
 * Given a live pathname (e.g. "/admin/permission/user" or "/admin/game-list/edit/123"),
 * return the routes/ subdirectory that owns it (e.g. "user", "game").
 *
 * Matching strategy: replace trailing numeric / uuid-like segments with ":id"
 * so the live URL aligns with the pattern stored during index build.
 */
export function getRouteSourceDir(pathname: string): string | null {
  // Normalise: strip trailing slash, replace numeric/uuid segments with :id
  const normalised = pathname
    .replace(/\/$/, '')
    .replace(/\/[0-9a-f]{8}-[0-9a-f-]{27,35}/gi, '/:id') // uuid
    .replace(/\/\d+/g, '/:id'); // numeric id

  return sourceDirIndex[normalised] ?? null;
}

// ─── renderRoutes() ──────────────────────────────────────────────────────────

function renderEntry(
  key: string,
  entry: RouteEntry,
): React.ReactElement | null {
  let element: React.ReactNode | undefined;
  if (entry.component) {
    const Comp = entry.component;
    element = (
      <S>
        <Comp />
      </S>
    );
  } else if (entry.wrapper) {
    const W = entry.wrapper;
    element = (
      <S>
        <W>
          <Outlet />
        </W>
      </S>
    );
  }

  if (entry.index) {
    return <Route key={key} index element={element ?? null} />;
  }

  const routePath = entry.path.split('?')[0];

  const childRoutes = Object.entries(entry.children ?? {})
    .map(([k, e]) => renderEntry(k, e))
    .filter(Boolean);

  // Sidebar-only: no element, no renderable children
  if (!element && childRoutes.length === 0) return null;

  return childRoutes.length > 0 ? (
    <Route key={key} path={routePath} element={element ?? <Outlet />}>
      {childRoutes}
    </Route>
  ) : (
    <Route key={key} path={routePath} element={element!} />
  );
}

// Memoized at module level — registry is static, routes never change
const cachedRoutes: React.ReactElement[] = Object.entries(routeRegistry)
  .map(([key, entry]) => renderEntry(key, entry))
  .filter(Boolean) as React.ReactElement[];

export function renderRoutes(): React.ReactElement[] {
  return cachedRoutes;
}
