import * as fs from 'node:fs';
import * as path from 'node:path';
import {
  readManifestFromPackageDir,
  readPackageJson,
  readVersionFromPackageDir,
} from './package-store.js';

type JsonRecord = Record<string, unknown>;

export interface IWebappDiscoveryMetadata {
  readonly appName: string;
  readonly routePrefix?: string;
  readonly displayName?: string;
  readonly icon?: string;
  readonly navOrder?: number;
  readonly description?: string;
  readonly shells: readonly string[];
}

export interface IComponentDiscoveryMetadata {
  readonly type: string;
  readonly exportName?: string;
  readonly mount?: string;
  readonly surface?: string;
  readonly description?: string;
  readonly kind?: string;
  readonly accepts: readonly string[];
  readonly emits: readonly string[];
  readonly skills: readonly string[];
  readonly tags: readonly string[];
  readonly capabilities: readonly string[];
}

export interface IPackageDiscoveryMetadata {
  readonly packageName: string;
  readonly version: string;
  readonly displayName?: string;
  readonly description?: string;
  readonly declaredKind?: string;
  readonly inferredKind: string;
  readonly tags: readonly string[];
  readonly capabilities: readonly string[];
  readonly permissions: JsonRecord | null;
  readonly webapp?: IWebappDiscoveryMetadata;
  readonly components: readonly IComponentDiscoveryMetadata[];
}

interface IClassStaticMetadata {
  readonly className: string;
  readonly description?: string;
  readonly kind?: string;
  readonly accepts: readonly string[];
  readonly emits: readonly string[];
  readonly skills: readonly string[];
  readonly tags: readonly string[];
  readonly capabilities: readonly string[];
}

function asRecord(value: unknown): JsonRecord | null {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
    ? value as JsonRecord
    : null;
}

function readOptionalString(value: unknown): string | undefined {
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}

function readOptionalNumber(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}

function readStringArray(value: unknown): readonly string[] {
  if (!Array.isArray(value)) {
    return [];
  }
  return value
    .filter((entry): entry is string => typeof entry === 'string')
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0);
}

function readObjectKeys(value: unknown): readonly string[] {
  const record = asRecord(value);
  return record ? Object.keys(record).sort((left, right) => left.localeCompare(right)) : [];
}

function inferPackageKind(manifest: JsonRecord): string {
  const webapp = asRecord(manifest.webapp);
  const components = Array.isArray(manifest.components) ? manifest.components : [];
  if (webapp && components.length > 0) {
    return 'app';
  }
  if (webapp) {
    return 'webapp';
  }
  if (components.length > 0) {
    return 'service';
  }
  return 'library';
}

function readPackageDescription(packageDir: string, manifest: JsonRecord): string | undefined {
  const manifestDescription = readOptionalString(manifest.description);
  if (manifestDescription) {
    return manifestDescription;
  }
  try {
    return readOptionalString(readPackageJson(packageDir).description);
  } catch {
    return undefined;
  }
}

function readWebappMetadata(manifest: JsonRecord): IWebappDiscoveryMetadata | undefined {
  const webapp = asRecord(manifest.webapp);
  const appName = readOptionalString(webapp?.appName);
  if (!webapp || !appName) {
    return undefined;
  }
  const shells = readStringArray(webapp.shells);
  return {
    appName,
    ...(readOptionalString(webapp.routePrefix) ? { routePrefix: readOptionalString(webapp.routePrefix) } : {}),
    ...(readOptionalString(webapp.displayName) ? { displayName: readOptionalString(webapp.displayName) } : {}),
    ...(readOptionalString(webapp.icon) ? { icon: readOptionalString(webapp.icon) } : {}),
    ...(readOptionalNumber(webapp.navOrder) !== undefined ? { navOrder: readOptionalNumber(webapp.navOrder) } : {}),
    ...(readOptionalString(webapp.description) ? { description: readOptionalString(webapp.description) } : {}),
    shells: shells.length > 0 ? shells : ['platform', 'edge'],
  };
}

function readComponentManifestEntry(value: unknown): IComponentDiscoveryMetadata | null {
  const record = asRecord(value);
  const type = readOptionalString(record?.type);
  if (!record || !type) {
    return null;
  }
  return {
    type,
    ...(readOptionalString(record.export) ? { exportName: readOptionalString(record.export) } : {}),
    ...(readOptionalString(record.mount) ? { mount: readOptionalString(record.mount) } : {}),
    ...(readOptionalString(record.surface) ? { surface: readOptionalString(record.surface) } : {}),
    ...(readOptionalString(record.description) ? { description: readOptionalString(record.description) } : {}),
    ...(readOptionalString(record.kind) ? { kind: readOptionalString(record.kind) } : {}),
    accepts: readStringArray(record.accepts).length > 0 ? readStringArray(record.accepts) : readObjectKeys(record.accepts),
    emits: readStringArray(record.emits).length > 0 ? readStringArray(record.emits) : readObjectKeys(record.emits),
    skills: readStringArray(record.skills),
    tags: readStringArray(record.tags),
    capabilities: readStringArray(record.capabilities),
  };
}

function mergeUnique(left: readonly string[], right: readonly string[]): readonly string[] {
  return [...new Set([...left, ...right])].sort((a, b) => a.localeCompare(b));
}

function enrichComponent(
  component: IComponentDiscoveryMetadata,
  classStatics: ReadonlyMap<string, IClassStaticMetadata>,
): IComponentDiscoveryMetadata {
  const statics = classStatics.get(component.type)
    ?? (component.exportName ? classStatics.get(component.exportName) : undefined);
  if (!statics) {
    return component;
  }
  return {
    ...component,
    description: component.description ?? statics.description,
    kind: component.kind ?? statics.kind,
    accepts: mergeUnique(component.accepts, statics.accepts),
    emits: mergeUnique(component.emits, statics.emits),
    skills: mergeUnique(component.skills, statics.skills),
    tags: mergeUnique(component.tags, statics.tags),
    capabilities: mergeUnique(component.capabilities, statics.capabilities),
  };
}

function readSourceFiles(packageDir: string): readonly string[] {
  const roots = ['src'];
  const runtimeEntry = readOptionalString(asRecord(readManifestFromPackageDir(packageDir).manifest.runtime)?.entry);
  if (runtimeEntry) {
    roots.push(path.dirname(runtimeEntry));
  }
  const sourceFiles: string[] = [];
  const visited = new Set<string>();

  function walk(dir: string): void {
    const resolved = path.resolve(dir);
    if (visited.has(resolved) || !fs.existsSync(resolved)) {
      return;
    }
    visited.add(resolved);
    for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
      if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) {
        continue;
      }
      const fullPath = path.join(resolved, entry.name);
      if (entry.isDirectory()) {
        walk(fullPath);
        continue;
      }
      if (entry.isFile() && /\.(?:ts|tsx|js|mjs|cjs)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
        sourceFiles.push(fullPath);
      }
    }
  }

  for (const root of roots) {
    walk(path.join(packageDir, root));
  }
  return [...new Set(sourceFiles)].sort((left, right) => left.localeCompare(right));
}

function findMatchingBrace(source: string, openIndex: number): number {
  let depth = 0;
  let quote: '"' | '\'' | '`' | null = null;
  let escaped = false;
  let lineComment = false;
  let blockComment = false;

  for (let index = openIndex; index < source.length; index += 1) {
    const char = source[index] ?? '';
    const next = source[index + 1] ?? '';

    if (lineComment) {
      if (char === '\n') lineComment = false;
      continue;
    }
    if (blockComment) {
      if (char === '*' && next === '/') {
        blockComment = false;
        index += 1;
      }
      continue;
    }
    if (quote) {
      if (escaped) {
        escaped = false;
        continue;
      }
      if (char === '\\') {
        escaped = true;
        continue;
      }
      if (char === quote) {
        quote = null;
      }
      continue;
    }
    if (char === '/' && next === '/') {
      lineComment = true;
      index += 1;
      continue;
    }
    if (char === '/' && next === '*') {
      blockComment = true;
      index += 1;
      continue;
    }
    if (char === '"' || char === '\'' || char === '`') {
      quote = char;
      continue;
    }
    if (char === '{') {
      depth += 1;
      continue;
    }
    if (char === '}') {
      depth -= 1;
      if (depth === 0) {
        return index;
      }
    }
  }
  return -1;
}

function readStaticString(classBody: string, fieldName: string): string | undefined {
  const pattern = new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=\\s*(['"\`])([\\s\\S]*?)\\1`);
  const match = pattern.exec(classBody);
  return readOptionalString(match?.[2]);
}

function readStaticArray(classBody: string, fieldName: string): readonly string[] {
  const fieldIndex = classBody.search(new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=`));
  if (fieldIndex < 0) {
    return [];
  }
  const openIndex = classBody.indexOf('[', fieldIndex);
  if (openIndex < 0) {
    return [];
  }
  const closeIndex = findMatchingBracket(classBody, openIndex, '[', ']');
  if (closeIndex < 0) {
    return [];
  }
  const body = classBody.slice(openIndex + 1, closeIndex);
  const values: string[] = [];
  const stringPattern = /(['"`])([\s\S]*?)\1/g;
  let match: RegExpExecArray | null;
  while ((match = stringPattern.exec(body)) !== null) {
    const value = readOptionalString(match[2]);
    if (value) values.push(value);
  }
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}

function findMatchingBracket(
  source: string,
  openIndex: number,
  openToken: '[' | '{',
  closeToken: ']' | '}',
): number {
  let depth = 0;
  let quote: '"' | '\'' | '`' | null = null;
  let escaped = false;
  for (let index = openIndex; index < source.length; index += 1) {
    const char = source[index] ?? '';
    if (quote) {
      if (escaped) {
        escaped = false;
        continue;
      }
      if (char === '\\') {
        escaped = true;
        continue;
      }
      if (char === quote) {
        quote = null;
      }
      continue;
    }
    if (char === '"' || char === '\'' || char === '`') {
      quote = char;
      continue;
    }
    if (char === openToken) {
      depth += 1;
      continue;
    }
    if (char === closeToken) {
      depth -= 1;
      if (depth === 0) {
        return index;
      }
    }
  }
  return -1;
}

function readStaticObjectKeys(classBody: string, fieldName: string): readonly string[] {
  const fieldIndex = classBody.search(new RegExp(`static\\s+${fieldName}\\s*(?::[^=;]+)?=`));
  if (fieldIndex < 0) {
    return [];
  }
  const openIndex = classBody.indexOf('{', fieldIndex);
  if (openIndex < 0) {
    return [];
  }
  const closeIndex = findMatchingBracket(classBody, openIndex, '{', '}');
  if (closeIndex < 0) {
    return [];
  }
  const objectText = classBody.slice(openIndex + 1, closeIndex);
  return readTopLevelObjectKeys(objectText);
}

function readTopLevelObjectKeys(objectText: string): readonly string[] {
  const keys: string[] = [];
  let index = 0;
  let depth = 0;
  let quote: '"' | '\'' | '`' | null = null;
  let escaped = false;
  let expectingKey = true;

  while (index < objectText.length) {
    const char = objectText[index] ?? '';
    if (quote) {
      if (escaped) {
        escaped = false;
      } else if (char === '\\') {
        escaped = true;
      } else if (char === quote) {
        quote = null;
      }
      index += 1;
      continue;
    }

    if (char === '"' || char === '\'' || char === '`') {
      if (depth === 0 && expectingKey) {
        const closeIndex = findStringEnd(objectText, index, char);
        if (closeIndex > index) {
          const key = readOptionalString(objectText.slice(index + 1, closeIndex));
          const afterKey = skipWhitespace(objectText, closeIndex + 1);
          if (key && objectText[afterKey] === ':') {
            keys.push(key);
            expectingKey = false;
            index = afterKey + 1;
            continue;
          }
        }
      }
      quote = char;
      index += 1;
      continue;
    }

    if (depth === 0 && expectingKey && /[A-Za-z_$]/.test(char)) {
      let endIndex = index + 1;
      while (endIndex < objectText.length && /[\w$-]/.test(objectText[endIndex] ?? '')) {
        endIndex += 1;
      }
      const key = objectText.slice(index, endIndex);
      const afterKey = skipWhitespace(objectText, endIndex);
      if (objectText[afterKey] === ':') {
        keys.push(key);
        expectingKey = false;
        index = afterKey + 1;
        continue;
      }
    }

    if (char === '{' || char === '[' || char === '(') {
      depth += 1;
    } else if (char === '}' || char === ']' || char === ')') {
      depth = Math.max(0, depth - 1);
    } else if (char === ',' && depth === 0) {
      expectingKey = true;
    }
    index += 1;
  }
  return [...new Set(keys)].sort((left, right) => left.localeCompare(right));
}

function skipWhitespace(source: string, startIndex: number): number {
  let index = startIndex;
  while (index < source.length && /\s/.test(source[index] ?? '')) {
    index += 1;
  }
  return index;
}

function findStringEnd(source: string, startIndex: number, quote: '"' | '\'' | '`'): number {
  let escaped = false;
  for (let index = startIndex + 1; index < source.length; index += 1) {
    const char = source[index] ?? '';
    if (escaped) {
      escaped = false;
      continue;
    }
    if (char === '\\') {
      escaped = true;
      continue;
    }
    if (char === quote) {
      return index;
    }
  }
  return -1;
}

function extractClassStaticsFromSource(source: string): readonly IClassStaticMetadata[] {
  const classes: IClassStaticMetadata[] = [];
  const classPattern = /(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\b/g;
  let match: RegExpExecArray | null;
  while ((match = classPattern.exec(source)) !== null) {
    const className = match[1] ?? '';
    const openIndex = source.indexOf('{', match.index + match[0].length);
    if (!className || openIndex < 0) {
      continue;
    }
    const closeIndex = findMatchingBrace(source, openIndex);
    if (closeIndex < 0) {
      continue;
    }
    const classBody = source.slice(openIndex + 1, closeIndex);
    classes.push({
      className,
      ...(readStaticString(classBody, 'description') ? { description: readStaticString(classBody, 'description') } : {}),
      ...(readStaticString(classBody, 'kind') ? { kind: readStaticString(classBody, 'kind') } : {}),
      accepts: readStaticObjectKeys(classBody, 'accepts'),
      emits: readStaticObjectKeys(classBody, 'emits'),
      skills: readStaticObjectKeys(classBody, 'skills'),
      tags: readStaticArray(classBody, 'tags'),
      capabilities: readStaticArray(classBody, 'capabilities'),
    });
    classPattern.lastIndex = closeIndex + 1;
  }
  return classes;
}

function extractClassStatics(packageDir: string): ReadonlyMap<string, IClassStaticMetadata> {
  const statics = new Map<string, IClassStaticMetadata>();
  for (const sourceFile of readSourceFiles(packageDir)) {
    const source = fs.readFileSync(sourceFile, 'utf8');
    for (const entry of extractClassStaticsFromSource(source)) {
      statics.set(entry.className, entry);
    }
  }
  return statics;
}

export function extractDiscoveryMetadata(packageDir: string): IPackageDiscoveryMetadata {
  const resolved = readManifestFromPackageDir(packageDir);
  const version = readVersionFromPackageDir(packageDir);
  const manifest = resolved.manifest;
  const classStatics = extractClassStatics(packageDir);
  const componentEntries = Array.isArray(manifest.components)
    ? manifest.components
      .map(readComponentManifestEntry)
      .filter((entry): entry is IComponentDiscoveryMetadata => Boolean(entry))
    : [];

  const webapp = readWebappMetadata(manifest);
  const declaredKind = readOptionalString(manifest.declaredKind)
    ?? readOptionalString(manifest.kind)
    ?? readOptionalString(manifest.class);
  const displayName = readOptionalString(manifest.displayName)
    ?? webapp?.displayName
    ?? resolved.packageName.split('/').at(-1);

  return {
    packageName: resolved.packageName,
    version,
    ...(displayName ? { displayName } : {}),
    ...(readPackageDescription(packageDir, manifest) ? { description: readPackageDescription(packageDir, manifest) } : {}),
    ...(declaredKind ? { declaredKind } : {}),
    inferredKind: inferPackageKind(manifest),
    tags: readStringArray(manifest.tags),
    capabilities: readStringArray(manifest.capabilities),
    permissions: asRecord(manifest.permissions),
    ...(webapp ? { webapp } : {}),
    components: componentEntries.map((component) => enrichComponent(component, classStatics)),
  };
}
