import { existsSync, statSync } from "node:fs";
import { dirname, extname, join, resolve } from "node:path";
import ts from "typescript";
import type { PropDefinition } from '@fragments-sdk/core';

export interface AutoDetectedPropDefinition {
  type: PropDefinition["type"];
  description: string;
  required: boolean;
  default?: unknown;
  values?: readonly string[];
}

export interface AutoPropsExtractionResult {
  props: Record<string, AutoDetectedPropDefinition>;
  warnings: string[];
  resolved: boolean;
}

interface ResolvedComponentSignature {
  propsTypeNode: ts.TypeNode | null;
  componentNode: ts.FunctionLikeDeclarationBase | null;
}

function toPosixPath(filePath: string): string {
  return filePath.replace(/\\/g, "/");
}

function isFile(filePath: string): boolean {
  if (!existsSync(filePath)) return false;
  try {
    return statSync(filePath).isFile();
  } catch {
    return false;
  }
}

function resolveModulePath(basePath: string): string | null {
  const candidates: string[] = [];
  const extension = extname(basePath);

  if (extension) {
    candidates.push(basePath);
  } else {
    candidates.push(
      `${basePath}.tsx`,
      `${basePath}.ts`,
      `${basePath}.jsx`,
      `${basePath}.js`,
      join(basePath, "index.tsx"),
      join(basePath, "index.ts"),
      join(basePath, "index.jsx"),
      join(basePath, "index.js")
    );
  }

  for (const candidate of candidates) {
    if (isFile(candidate)) {
      return resolve(candidate);
    }
  }

  return null;
}

/**
 * Resolve a component source file from the fragment file path + component import path.
 * Supports relative imports like ".", "./Button", "../components/Button".
 */
export function resolveComponentSourcePath(
  fragmentFileAbsolutePath: string,
  componentImportPath: string | null
): string | null {
  if (!componentImportPath) return null;
  if (!componentImportPath.startsWith(".")) return null;

  const fragmentDir = dirname(fragmentFileAbsolutePath);
  const basePath = resolve(fragmentDir, componentImportPath);
  return resolveModulePath(basePath);
}

function collectTopLevelDeclarations(sourceFile: ts.SourceFile): {
  typeDeclarations: Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>;
  functionDeclarations: Map<string, ts.FunctionDeclaration>;
  variableDeclarations: Map<string, ts.VariableDeclaration>;
} {
  const typeDeclarations = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
  const functionDeclarations = new Map<string, ts.FunctionDeclaration>();
  const variableDeclarations = new Map<string, ts.VariableDeclaration>();

  for (const node of sourceFile.statements) {
    if (ts.isInterfaceDeclaration(node)) {
      typeDeclarations.set(node.name.text, node);
      continue;
    }

    if (ts.isTypeAliasDeclaration(node)) {
      typeDeclarations.set(node.name.text, node);
      continue;
    }

    if (ts.isFunctionDeclaration(node) && node.name) {
      functionDeclarations.set(node.name.text, node);
      continue;
    }

    if (ts.isVariableStatement(node)) {
      for (const declaration of node.declarationList.declarations) {
        if (ts.isIdentifier(declaration.name)) {
          variableDeclarations.set(declaration.name.text, declaration);
        }
      }
    }
  }

  return { typeDeclarations, functionDeclarations, variableDeclarations };
}

function readDefaultValue(expression: ts.Expression): unknown {
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
    return expression.text;
  }

  if (ts.isNumericLiteral(expression)) {
    return Number(expression.text);
  }

  if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
  if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
  if (expression.kind === ts.SyntaxKind.NullKeyword) return null;

  if (
    ts.isPrefixUnaryExpression(expression) &&
    expression.operator === ts.SyntaxKind.MinusToken &&
    ts.isNumericLiteral(expression.operand)
  ) {
    return -Number(expression.operand.text);
  }

  return undefined;
}

function extractDefaultValues(
  componentNode: ts.FunctionLikeDeclarationBase | null
): Record<string, unknown> {
  const defaults: Record<string, unknown> = {};
  if (!componentNode?.parameters?.length) return defaults;

  const firstParam = componentNode.parameters[0];
  if (!ts.isObjectBindingPattern(firstParam.name)) return defaults;

  for (const element of firstParam.name.elements) {
    let propName: string | null = null;

    if (element.propertyName) {
      if (ts.isIdentifier(element.propertyName) || ts.isStringLiteral(element.propertyName)) {
        propName = element.propertyName.text;
      }
    } else if (ts.isIdentifier(element.name)) {
      propName = element.name.text;
    }

    if (!propName || !element.initializer) continue;

    const value = readDefaultValue(element.initializer);
    if (value !== undefined) {
      defaults[propName] = value;
    }
  }

  return defaults;
}

function isNullishType(type: ts.Type): boolean {
  return (
    (type.flags & ts.TypeFlags.Null) !== 0 ||
    (type.flags & ts.TypeFlags.Undefined) !== 0 ||
    (type.flags & ts.TypeFlags.Void) !== 0
  );
}

function isBooleanLikeType(type: ts.Type): boolean {
  return (
    (type.flags & ts.TypeFlags.BooleanLike) !== 0 ||
    type.flags === ts.TypeFlags.BooleanLiteral
  );
}

function inferPropType(
  type: ts.Type,
  checker: ts.TypeChecker
): Pick<AutoDetectedPropDefinition, "type" | "values"> {
  const typeText = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);

  if (typeText.includes("ReactNode")) {
    return { type: "node" };
  }
  if (typeText.includes("ReactElement") || typeText.includes("JSX.Element")) {
    return { type: "element" };
  }

  if (type.getCallSignatures().length > 0) {
    return { type: "function" };
  }

  if (checker.isArrayType(type) || checker.isTupleType(type)) {
    return { type: "array" };
  }

  if (type.isUnion()) {
    const nonNullableTypes = type.types.filter((unionType) => !isNullishType(unionType));

    if (nonNullableTypes.length === 1) {
      return inferPropType(nonNullableTypes[0], checker);
    }

    const stringLiteralValues = nonNullableTypes
      .filter((unionType) => (unionType.flags & ts.TypeFlags.StringLiteral) !== 0)
      .map((unionType) => (unionType as ts.StringLiteralType).value);

    if (stringLiteralValues.length > 0 && stringLiteralValues.length === nonNullableTypes.length) {
      return { type: "enum", values: stringLiteralValues };
    }

    if (nonNullableTypes.every((unionType) => isBooleanLikeType(unionType))) {
      return { type: "boolean" };
    }

    return { type: "union" };
  }

  if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
    return { type: "string" };
  }
  if ((type.flags & ts.TypeFlags.NumberLike) !== 0) {
    return { type: "number" };
  }
  if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) {
    return { type: "boolean" };
  }

  if ((type.flags & ts.TypeFlags.Object) !== 0) {
    return { type: "object" };
  }

  return { type: "custom" };
}

function resolveComponentSignature(
  exportName: string,
  declarations: ReturnType<typeof collectTopLevelDeclarations>,
  sourceFile: ts.SourceFile
): ResolvedComponentSignature {
  const visitedNames = new Set<string>();

  const typeNodeFromFunction = (
    node: ts.FunctionLikeDeclarationBase
  ): ResolvedComponentSignature => ({
    propsTypeNode: node.parameters[0]?.type ?? null,
    componentNode: node,
  });

  const resolveFromExpression = (expression: ts.Expression): ResolvedComponentSignature => {
    if (ts.isParenthesizedExpression(expression)) {
      return resolveFromExpression(expression.expression);
    }
    if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
      return resolveFromExpression(expression.expression);
    }
    if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
      return typeNodeFromFunction(expression);
    }
    if (ts.isIdentifier(expression)) {
      return resolveFromIdentifier(expression.text);
    }

    if (ts.isCallExpression(expression)) {
      if (
        ts.isPropertyAccessExpression(expression.expression) &&
        expression.expression.name.text === "forwardRef"
      ) {
        const forwardRefPropsType = expression.typeArguments?.[1] ?? null;
        const innerArg = expression.arguments[0];
        const inner = innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))
          ? typeNodeFromFunction(innerArg)
          : innerArg && ts.isIdentifier(innerArg)
          ? resolveFromIdentifier(innerArg.text)
          : { propsTypeNode: null, componentNode: null };

        return {
          propsTypeNode: forwardRefPropsType ?? inner.propsTypeNode,
          componentNode: inner.componentNode,
        };
      }

      if (
        ts.isPropertyAccessExpression(expression.expression) &&
        expression.expression.name.text === "memo" &&
        expression.arguments[0]
      ) {
        return resolveFromExpression(expression.arguments[0]);
      }

      if (
        ts.isPropertyAccessExpression(expression.expression) &&
        expression.expression.expression.getText(sourceFile) === "Object" &&
        expression.expression.name.text === "assign" &&
        expression.arguments[0]
      ) {
        return resolveFromExpression(expression.arguments[0]);
      }
    }

    return { propsTypeNode: null, componentNode: null };
  };

  const resolveFromVariable = (declaration: ts.VariableDeclaration): ResolvedComponentSignature => {
    if (
      declaration.type &&
      ts.isTypeReferenceNode(declaration.type) &&
      declaration.type.typeArguments?.length
    ) {
      const typeName = declaration.type.typeName.getText(sourceFile);
      if (typeName.includes("FC") || typeName.includes("FunctionComponent")) {
        const componentNode =
          declaration.initializer &&
          (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
            ? declaration.initializer
            : null;

        return {
          propsTypeNode: declaration.type.typeArguments[0] ?? null,
          componentNode,
        };
      }
    }

    if (declaration.initializer) {
      return resolveFromExpression(declaration.initializer);
    }

    return { propsTypeNode: null, componentNode: null };
  };

  const resolveFromIdentifier = (name: string): ResolvedComponentSignature => {
    if (!name || visitedNames.has(name)) {
      return { propsTypeNode: null, componentNode: null };
    }
    visitedNames.add(name);

    const functionDeclaration = declarations.functionDeclarations.get(name);
    if (functionDeclaration) {
      return typeNodeFromFunction(functionDeclaration);
    }

    const variableDeclaration = declarations.variableDeclarations.get(name);
    if (variableDeclaration) {
      return resolveFromVariable(variableDeclaration);
    }

    return { propsTypeNode: null, componentNode: null };
  };

  return resolveFromIdentifier(exportName);
}

/**
 * Extract custom component props from a source file.
 * Custom props are identified as props whose declarations originate from the component's source file.
 */
export function extractCustomPropsFromComponentFile(
  componentFilePath: string,
  exportName: string
): AutoPropsExtractionResult {
  const warnings: string[] = [];
  const resolvedPath = resolve(componentFilePath);

  if (!existsSync(resolvedPath)) {
    return {
      props: {},
      warnings: [`Component file not found: ${resolvedPath}`],
      resolved: false,
    };
  }

  const compilerOptions: ts.CompilerOptions = {
    target: ts.ScriptTarget.ESNext,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.Bundler,
    jsx: ts.JsxEmit.ReactJSX,
    allowSyntheticDefaultImports: true,
    esModuleInterop: true,
    skipLibCheck: true,
    strict: false,
    noEmit: true,
  };

  const program = ts.createProgram([resolvedPath], compilerOptions);
  const sourceFile = program.getSourceFile(resolvedPath);
  if (!sourceFile) {
    return {
      props: {},
      warnings: [`Unable to parse component source: ${resolvedPath}`],
      resolved: false,
    };
  }

  const checker = program.getTypeChecker();
  const declarations = collectTopLevelDeclarations(sourceFile);
  const signature = resolveComponentSignature(exportName, declarations, sourceFile);

  if (!signature.propsTypeNode) {
    return {
      props: {},
      warnings: [`Unable to resolve props type for export: ${exportName}`],
      resolved: false,
    };
  }

  const propsType = checker.getTypeFromTypeNode(signature.propsTypeNode);
  const defaultValues = extractDefaultValues(signature.componentNode);
  const sourceFilePath = toPosixPath(sourceFile.fileName);

  const extractedProps: Record<string, AutoDetectedPropDefinition> = {};
  for (const symbol of checker.getPropertiesOfType(propsType)) {
    const propName = symbol.getName();
    if (propName.startsWith("_") || propName.startsWith("$")) {
      continue;
    }

    const declarationsForSymbol = symbol.getDeclarations() ?? [];
    const localDeclarations = declarationsForSymbol.filter(
      (declaration) => toPosixPath(declaration.getSourceFile().fileName) === sourceFilePath
    );

    if (localDeclarations.length === 0) {
      continue;
    }

    const referenceNode = localDeclarations[0];
    const inferredType = inferPropType(checker.getTypeOfSymbolAtLocation(symbol, referenceNode), checker);
    const description = ts
      .displayPartsToString(symbol.getDocumentationComment(checker))
      .trim();

    extractedProps[propName] = {
      type: inferredType.type,
      description,
      required: (symbol.getFlags() & ts.SymbolFlags.Optional) === 0,
      ...(inferredType.values && { values: inferredType.values }),
      ...(defaultValues[propName] !== undefined && { default: defaultValues[propName] }),
    };
  }

  if (Object.keys(extractedProps).length === 0) {
    warnings.push(`Resolved props type for ${exportName}, but no local custom props were found`);
  }

  return {
    props: extractedProps,
    warnings,
    resolved: true,
  };
}
