/**
 * Route compilation + matching.
 *
 * Supported segments:
 *   [id]          required parameter
 *   [id?]         optional parameter
 *   [[id]]        optional parameter (directory-friendly form)
 *   [...slug]     required catch-all
 *   [[...slug]]   optional catch-all
 */

export interface RouteParam {
  name: string;
  optional: boolean;
  catchAll: boolean;
}

export interface Route {
  /** The human-readable route pattern, e.g. `/users/[id]`. */
  raw: string;
  /** Absolute path to the module that handles this route. */
  file: string;
  /** Compiled matcher. */
  regex: RegExp;
  /** Ordered names of dynamic params captured by `regex`. */
  paramNames: string[];
  /** Rich parameter metadata. Optional for compatibility with old manifests. */
  paramMeta?: RouteParam[];
}

export interface RouteMatch {
  route: Route;
  params: Record<string, string>;
}

const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;

function parseParamSegment(segment: string): RouteParam | null {
  let inner: string | null = null;
  let optional = false;

  if (segment.startsWith("[[") && segment.endsWith("]]")) {
    inner = segment.slice(2, -2);
    optional = true;
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
    inner = segment.slice(1, -1);
    if (inner.endsWith("?")) {
      optional = true;
      inner = inner.slice(0, -1);
    }
  }

  if (inner === null) return null;
  const catchAll = inner.startsWith("...");
  const name = catchAll ? inner.slice(3) : inner;
  if (!/^[A-Za-z_$][\w$-]*$/.test(name)) {
    throw new Error(`WRN-ROUTE-PARAM: Invalid route parameter '${segment}'.`);
  }
  return { name, optional, catchAll };
}

/** Return parameter metadata without requiring callers to inspect the regex. */
export function getRouteParams(raw: string): RouteParam[] {
  return raw
    .split("/")
    .filter(Boolean)
    .map(parseParamSegment)
    .filter((value): value is RouteParam => value !== null);
}

/** Compile a WRNexus route pattern into a RegExp + parameter metadata. */
export function compileRoutePattern(
  raw: string,
): Pick<Route, "regex" | "paramNames" | "paramMeta"> {
  if (raw === "/") {
    return { regex: /^\/$/, paramNames: [], paramMeta: [] };
  }

  const paramMeta: RouteParam[] = [];
  let source = "^";
  const segments = raw.split("/").filter(Boolean);

  for (const segment of segments) {
    const param = parseParamSegment(segment);
    if (!param) {
      source += `/${segment.replace(ESCAPE_RE, "\\$&")}`;
      continue;
    }

    if (paramMeta.some((existing) => existing.name === param.name)) {
      throw new Error(
        `WRN-ROUTE-DUPLICATE-PARAM: Parameter '${param.name}' appears more than once in '${raw}'.`,
      );
    }
    paramMeta.push(param);

    const capture = param.catchAll ? "(.+?)" : "([^/]+)";
    source += param.optional ? `(?:/${capture})?` : `/${capture}`;
  }

  source += "/?$";
  return {
    regex: new RegExp(source),
    paramNames: paramMeta.map((param) => param.name),
    paramMeta,
  };
}

function routeSpecificity(route: Route): number[] {
  const segments = route.raw.split("/").filter(Boolean);
  let staticCount = 0;
  let requiredCount = 0;
  let optionalCount = 0;
  let catchAllCount = 0;
  for (const segment of segments) {
    const param = parseParamSegment(segment);
    if (!param) staticCount++;
    else if (param.catchAll) catchAllCount++;
    else if (param.optional) optionalCount++;
    else requiredCount++;
  }
  return [
    staticCount,
    requiredCount,
    -optionalCount,
    -catchAllCount,
    segments.length,
    route.raw.length,
  ];
}

/**
 * Order routes so static and constrained routes win over optional/catch-all
 * routes. The ordering remains deterministic for identical specificity.
 */
export function sortRoutes(routes: Route[]): Route[] {
  return [...routes].sort((a, b) => {
    const as = routeSpecificity(a);
    const bs = routeSpecificity(b);
    for (let i = 0; i < as.length; i++) {
      if (as[i] !== bs[i]) return bs[i]! - as[i]!;
    }
    return a.raw.localeCompare(b.raw) || a.file.localeCompare(b.file);
  });
}

/** Find duplicate URL patterns before request handling starts. */
export function findRouteConflicts(routes: Route[]): Array<{ raw: string; files: string[] }> {
  const grouped = new Map<string, string[]>();
  for (const route of routes) {
    const files = grouped.get(route.raw) ?? [];
    files.push(route.file);
    grouped.set(route.raw, files);
  }
  return [...grouped]
    .filter(([, files]) => files.length > 1)
    .map(([raw, files]) => ({ raw, files }));
}

/** Find the first route whose pattern matches `pathname`. */
export function matchRoute(routes: Route[], pathname: string): RouteMatch | null {
  for (const route of routes) {
    const match = route.regex.exec(pathname);
    if (!match) continue;
    const params: Record<string, string> = {};
    try {
      route.paramNames.forEach((name, index) => {
        const value = match[index + 1];
        if (value !== undefined) params[name] = decodeURIComponent(value);
      });
    } catch {
      // A malformed percent-encoded path is not a valid route match. Treat it
      // as a 404 instead of allowing decodeURIComponent to become a 500.
      return null;
    }
    return { route, params };
  }
  return null;
}
