import {
  autocompletion,
} from "@codemirror/autocomplete";
import type {
  CompletionContext,
  CompletionResult,
  Completion,
} from "@codemirror/autocomplete";
import { syntaxTree } from "@codemirror/language";
import type { Extension } from "@codemirror/state";

// --- dexpr type system ---

export type DexprType =
  | "String"
  | "Number"
  | "Boolean"
  | "NumberList"
  | "StringList"
  | "Object"
  | "List";

export interface FunctionInfo {
  name: string;
  signature: string;
  doc?: string;
}

export interface MethodInfo {
  name: string;
  signature: string;
  doc?: string;
}

export interface FieldInfo {
  name: string;
  type: DexprType;
}

export interface VariableInfo {
  name: string;
  type: DexprType;
  doc?: string;
  fields?: FieldInfo[];
}

/**
 * Language metadata — generated by Rust `LanguageInfo::to_json()`,
 * extended with host-registered functions/methods/variables.
 */
export interface DexprLanguageInfo {
  functions: FunctionInfo[];
  methods: Partial<Record<DexprType, MethodInfo[]>>;
  variables?: VariableInfo[];
}

// --- Build completions from metadata ---

function funcToCompletion(f: FunctionInfo): Completion {
  return {
    label: f.name,
    type: "function",
    detail: f.signature,
    info: f.doc,
  };
}

function methodToCompletion(m: MethodInfo): Completion {
  return {
    label: m.name,
    type: "method",
    detail: m.signature,
    info: m.doc,
  };
}

function varToCompletion(v: VariableInfo): Completion {
  return {
    label: v.name,
    type: "variable",
    detail: v.type,
    info: v.doc,
  };
}

const KEYWORDS: Completion[] = [
  { label: "if", type: "keyword" },
  { label: "then", type: "keyword" },
  { label: "else", type: "keyword" },
  { label: "end", type: "keyword" },
  { label: "true", type: "keyword", detail: "Boolean" },
  { label: "false", type: "keyword", detail: "Boolean" },
  { label: "null", type: "keyword", detail: "Null" },
  { label: "in", type: "keyword", detail: "membership test" },
];

// --- Type inference from Lezer tree ---

/**
 * Walk the tree to infer variable types from assignments.
 * Scans `Assignment` nodes: `VariableName AssignOp expression`
 * and determines the type of the right-hand side expression.
 */
function inferVariableTypes(
  context: CompletionContext,
  knownVars: Map<string, DexprType>
): Map<string, DexprType> {
  const types = new Map(knownVars);
  const tree = syntaxTree(context.state);
  const doc = context.state.doc;

  tree.iterate({
    enter(node) {
      if (node.name !== "Assignment") return;

      // First child: VariableName
      const varNode = node.node.firstChild;
      if (!varNode || varNode.name !== "VariableName") return;
      const varName = doc.sliceString(varNode.from, varNode.to);

      // Third child (skip AssignOp): expression
      const assignOp = varNode.nextSibling;
      if (!assignOp) return;
      const exprNode = assignOp.nextSibling;
      if (!exprNode) return;

      const exprType = inferExprType(exprNode, doc, types);
      if (exprType) types.set(varName, exprType);
    },
  });

  return types;
}

function inferExprType(
  node: { name: string; from: number; to: number; firstChild: any },
  doc: { sliceString(from: number, to: number): string },
  knownTypes: Map<string, DexprType>
): DexprType | null {
  switch (node.name) {
    case "String":
      return "String";
    case "Number":
      return "Number";
    case "BooleanLiteral":
      return "Boolean";
    case "VariableName": {
      const name = doc.sliceString(node.from, node.to);
      return knownTypes.get(name) ?? null;
    }
    case "MethodCall": {
      // Infer return type from method name
      const propNode = findChild(node, "PropertyName");
      if (!propNode) return null;
      const method = doc.sliceString(propNode.from, propNode.to);
      return inferMethodReturnType(method);
    }
    case "BinaryExpression": {
      // String + anything = String, Number ops = Number
      const first = node.firstChild;
      if (first) {
        const t = inferExprType(first, doc, knownTypes);
        if (t === "String") return "String";
        if (t === "Number") return "Number";
      }
      return null;
    }
    case "PropertyAccess": {
      // Resolve: obj.field → look up field type from config
      const objNode = node.firstChild;
      const propNode = findChild(node, "PropertyName");
      if (!objNode || !propNode) return null;
      if (objNode.name === "VariableName") {
        const varName = doc.sliceString(objNode.from, objNode.to);
        const fieldName = doc.sliceString(propNode.from, propNode.to);
        const rootType = knownTypes.get(varName);
        if (rootType === "Object") {
          return knownTypes.get(`${varName}.${fieldName}`) ?? null;
        }
        if (rootType === "List") {
          // Property projection: list.field → typed list based on field type
          const fieldType = knownTypes.get(`${varName}.${fieldName}`) ?? null;
          return projectedListType(fieldType);
        }
      }
      return null;
    }
    case "FunctionCall":
    case "ParenExpression":
      return null;
    default:
      return null;
  }
}

function findChild(
  node: { firstChild: any },
  name: string
): { name: string; from: number; to: number } | null {
  let child = node.firstChild;
  while (child) {
    if (child.name === name) return child;
    child = child.nextSibling;
  }
  return null;
}

/** Infer return type from known method names */
export function inferMethodReturnType(method: string): DexprType | null {
  switch (method) {
    // String -> String
    case "upper":
    case "lower":
    case "trim":
    case "trimStart":
    case "trimEnd":
    case "replace":
    case "charAt":
    case "substring":
      return "String";
    // String -> Boolean
    case "contains":
    case "startsWith":
    case "endsWith":
    case "isEmpty":
      return "Boolean";
    // String -> Number
    case "length":
    case "len":
    case "indexOf":
      return "Number";
    // String -> StringList
    case "split":
      return "StringList";
    // List -> aggregate
    case "sum":
    case "avg":
    case "min":
    case "max":
    case "first":
    case "last":
      return "Number";
    // List methods returning lists
    case "reverse":
    case "sort":
    case "slice":
      return null; // depends on input type
    case "join":
      return "String";
    // List methods
    case "map":
      return null; // depends on field type (NumberList, StringList, or List)
    case "filter":
      return "List";
    case "find":
      return null; // returns single element
    default:
      return null;
  }
}

/**
 * Given a field type from an Object element within a List,
 * return the projected list type after property access.
 * e.g. List with Number field "tutar" → kalemler.tutar → NumberList
 */
export function projectedListType(fieldType: DexprType | null): DexprType {
  if (fieldType === "Number") return "NumberList";
  if (fieldType === "String") return "StringList";
  return "List";
}

// --- Autocomplete ---

function dedup(items: Completion[]): Completion[] {
  const seen = new Set<string>();
  return items.filter((item) => {
    if (seen.has(item.label)) return false;
    seen.add(item.label);
    return true;
  });
}

export function dexprCompletion(info: DexprLanguageInfo): Extension {
  const functionCompletions = info.functions.map(funcToCompletion);
  const variableCompletions = (info.variables ?? []).map(varToCompletion);

  // Methods per type
  const methodsByType: Record<string, Completion[]> = {};
  for (const [type, methods] of Object.entries(info.methods)) {
    methodsByType[type] = (methods ?? []).map(methodToCompletion);
  }

  const allMethods = dedup(
    Object.values(methodsByType).flat()
  );
  const allIdentifiers = [
    ...KEYWORDS,
    ...functionCompletions,
    ...variableCompletions,
  ];

  // Build known variable type map from config
  const configVarTypes = new Map<string, DexprType>();
  for (const v of info.variables ?? []) {
    configVarTypes.set(v.name, v.type);
  }

  // Build Object field type lookup: "varName.fieldName" → DexprType
  // and field completions per Object variable
  const objectFieldCompletions = new Map<string, Completion[]>();
  for (const v of info.variables ?? []) {
    if ((v.type === "Object" || v.type === "List") && v.fields) {
      const fieldItems: Completion[] = [];
      for (const f of v.fields) {
        // Store "customer.name" → "String" in configVarTypes for type inference
        configVarTypes.set(`${v.name}.${f.name}`, f.type);
        fieldItems.push({
          label: f.name,
          type: "property",
          detail: f.type,
        });
      }
      objectFieldCompletions.set(v.name, fieldItems);
    }
  }

  /**
   * Resolve the type of a dotted path expression before the cursor.
   * Walks the Lezer tree backwards from a dot position to build
   * the full path (e.g. "customer.address") and looks up field types.
   */
  function resolveDotPath(
    context: CompletionContext,
    dotPos: number,
    varTypes: Map<string, DexprType>
  ): { type: DexprType | null; path: string[] } {
    const tree = syntaxTree(context.state);
    const doc = context.state.doc;

    // Collect the chain of identifiers before the dot
    // e.g. for "customer.address.|" we want ["customer", "address"]
    const path: string[] = [];
    let pos = dotPos;

    // Walk backwards through PropertyAccess / MethodCall nodes
    while (true) {
      const nodeAtPos = tree.resolveInner(pos, -1);

      if (nodeAtPos.name === "PropertyName") {
        path.unshift(doc.sliceString(nodeAtPos.from, nodeAtPos.to));
        // Skip backwards past the "." to the expression before it
        const dotCharPos = nodeAtPos.from - 1;
        if (dotCharPos >= 0 && doc.sliceString(dotCharPos, dotCharPos + 1) === ".") {
          pos = dotCharPos;
          continue;
        }
        break;
      } else if (nodeAtPos.name === "VariableName") {
        path.unshift(doc.sliceString(nodeAtPos.from, nodeAtPos.to));
        break;
      } else {
        break;
      }
    }

    if (path.length === 0) return { type: null, path };

    // Resolve the type by walking the path
    const rootType = varTypes.get(path[0]) ?? null;
    if (path.length === 1) return { type: rootType, path };

    // For multi-segment paths, look up field types
    // e.g. path=["customer","name"] → look up "customer.name" in varTypes
    let currentType = rootType;
    for (let i = 1; i < path.length; i++) {
      if (currentType === "Object") {
        const key = `${path[i - 1]}.${path[i]}`;
        currentType = varTypes.get(key) ?? null;
      } else if (currentType === "List") {
        // Property projection: list.field → typed list
        const key = `${path[0]}.${path[i]}`;
        const fieldType = varTypes.get(key) ?? null;
        currentType = projectedListType(fieldType);
      } else {
        return { type: currentType, path };
      }
    }

    return { type: currentType, path };
  }

  function completions(context: CompletionContext): CompletionResult | null {
    const tree = syntaxTree(context.state);
    const node = tree.resolveInner(context.pos, -1);

    // Don't complete inside strings or comments
    if (
      node.name === "String" ||
      node.name === "LineComment" ||
      node.name === "BlockComment"
    )
      return null;

    // Method/property completion: after "."
    const dotMatch = context.matchBefore(/\.\w*/);
    if (dotMatch) {
      const dotPos = dotMatch.from;
      const beforeNode = tree.resolveInner(dotPos, -1);

      const varTypes = inferVariableTypes(context, configVarTypes);

      if (beforeNode.name === "Number") {
        return null; // could be decimal
      }

      // Try to resolve the full dotted path for type info
      const { type: resolvedType, path } = resolveDotPath(context, dotPos, varTypes);

      // For literal types, resolve directly
      let finalType = resolvedType;
      if (!finalType) {
        if (beforeNode.name === "String") finalType = "String";
        else if (beforeNode.name === "BooleanLiteral") finalType = "Boolean";
      }

      let options: Completion[];

      if (finalType === "Object") {
        // Show field names + Object methods
        const rootVarName = path[0];
        const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
        const objMethods = methodsByType["Object"] ?? [];
        options = [...fieldItems, ...objMethods];
      } else if (finalType === "List") {
        // Show field names (property projection) + List methods
        const rootVarName = path[0];
        const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
        const listMethods = methodsByType["List"] ?? [];
        options = [...fieldItems, ...listMethods];
      } else if (finalType) {
        options = methodsByType[finalType] ?? allMethods;
      } else {
        options = allMethods;
      }

      if (options.length === 0) return null;

      return {
        from: dotMatch.from + 1,
        options,
        validFor: /^\w*$/,
      };
    }

    // Identifier/keyword completion
    const wordMatch = context.matchBefore(/[a-zA-Z_]\w*/);
    if (!wordMatch && !context.explicit) return null;
    if (wordMatch && wordMatch.from === wordMatch.to && !context.explicit)
      return null;

    return {
      from: wordMatch?.from ?? context.pos,
      options: allIdentifiers,
      validFor: /^\w*$/,
    };
  }

  return autocompletion({ override: [completions] });
}

export { KEYWORDS };
