import type { CompletionContext, CompletionResult, Completion } from "@codemirror/autocomplete";
import {
  type TableSchema,
  type ColumnSchema,
  TSQL_CLICKHOUSE_FUNCTIONS,
  TSQL_AGGREGATIONS,
} from "@internal/tsql";

/**
 * SQL keywords for autocomplete
 */
const SQL_KEYWORDS = [
  "SELECT",
  "FROM",
  "WHERE",
  "AND",
  "OR",
  "NOT",
  "IN",
  "LIKE",
  "ILIKE",
  "BETWEEN",
  "IS",
  "NULL",
  "TRUE",
  "FALSE",
  "AS",
  "ORDER",
  "BY",
  "ASC",
  "DESC",
  "LIMIT",
  "OFFSET",
  "GROUP",
  "HAVING",
  "DISTINCT",
  "JOIN",
  "LEFT",
  "RIGHT",
  "INNER",
  "OUTER",
  "FULL",
  "CROSS",
  "ON",
  "UNION",
  "INTERSECT",
  "EXCEPT",
  "ALL",
  "WITH",
  "CASE",
  "WHEN",
  "THEN",
  "ELSE",
  "END",
  "OVER",
  "PARTITION",
  "ROWS",
  "RANGE",
  "UNBOUNDED",
  "PRECEDING",
  "FOLLOWING",
  "CURRENT",
  "ROW",
  "NULLS",
  "FIRST",
  "LAST",
];

/**
 * Create keyword completions from the SQL keywords list
 */
function createKeywordCompletions(): Completion[] {
  return SQL_KEYWORDS.map((keyword) => ({
    label: keyword,
    type: "keyword",
    boost: -1, // Keywords should have lower priority than schema items
  }));
}

/**
 * Create function completions from TSQL function definitions
 */
function createFunctionCompletions(): Completion[] {
  const functions: Completion[] = [];

  // Add regular functions
  for (const [name, meta] of Object.entries(TSQL_CLICKHOUSE_FUNCTIONS)) {
    // Skip internal functions starting with _
    if (name.startsWith("_")) continue;

    const argsHint =
      meta.maxArgs === 0
        ? "()"
        : meta.minArgs === meta.maxArgs
        ? `(${meta.minArgs} args)`
        : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;

    functions.push({
      label: name,
      type: "function",
      detail: argsHint,
      apply: `${name}()`,
    });
  }

  // Add aggregate functions with slightly higher boost
  for (const [name, meta] of Object.entries(TSQL_AGGREGATIONS)) {
    if (name.startsWith("_")) continue;

    const argsHint =
      meta.maxArgs === 0
        ? "()"
        : meta.minArgs === meta.maxArgs
        ? `(${meta.minArgs} args)`
        : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;

    functions.push({
      label: name,
      type: "function",
      detail: `aggregate ${argsHint}`,
      apply: `${name}()`,
      boost: 0.5,
    });
  }

  // Add special TSQL functions not in the ClickHouse function registry
  functions.push({
    label: "timeBucket",
    type: "function",
    detail: "auto time bucket (0 args)",
    apply: "timeBucket()",
    boost: 1.5,
    info: "Automatically bucket by time using the table's time column. Interval is chosen based on the query's time range.",
  });

  return functions;
}

/**
 * Create table completions from schema
 */
function createTableCompletions(schema: TableSchema[]): Completion[] {
  return schema.map((table) => ({
    label: table.name,
    type: "class", // Using "class" type for tables gives them a nice icon
    detail: table.description || "table",
    boost: 1, // Tables should have higher priority
  }));
}

/**
 * Create column completions for a specific table
 */
function createColumnCompletions(table: TableSchema, prefix?: string): Completion[] {
  const columns: Completion[] = [];

  for (const [name, column] of Object.entries(table.columns)) {
    columns.push({
      label: prefix ? `${prefix}.${name}` : name,
      type: "property", // Using "property" type for columns
      detail: `${column.type}${column.description ? ` - ${column.description}` : ""}`,
      boost: 2, // Columns should have highest priority
    });
  }

  return columns;
}

/**
 * Extract table names/aliases from the current query context
 * This is a simplified parser that looks for FROM and JOIN clauses
 */
function extractTablesFromQuery(doc: string, schema: TableSchema[]): Map<string, TableSchema> {
  const tableMap = new Map<string, TableSchema>();
  const tableNames = schema.map((t) => t.name);

  // Simple regex to find table references in FROM and JOIN clauses
  // Handles: FROM table_name, FROM table_name AS alias, FROM table_name alias
  const tablePattern = /(?:FROM|JOIN)\s+(\w+)(?:\s+(?:AS\s+)?(\w+))?/gi;

  let match;
  while ((match = tablePattern.exec(doc)) !== null) {
    const tableName = match[1];
    const alias = match[2] || tableName;

    // Find the table schema if it exists
    const tableSchema = schema.find((t) => t.name.toLowerCase() === tableName.toLowerCase());

    if (tableSchema) {
      tableMap.set(alias.toLowerCase(), tableSchema);
    }
  }

  return tableMap;
}

/**
 * Determine what context we're in based on cursor position
 */
type CompletionContextType =
  | "table" // After FROM or JOIN
  | "column" // After SELECT, WHERE, ORDER BY, GROUP BY, etc.
  | "alias" // After table_name.
  | "value" // After comparison operator (=, !=, IN, etc.)
  | "general"; // Anywhere else

/**
 * Result of context detection
 */
interface ContextResult {
  type: CompletionContextType;
  tablePrefix?: string;
  /** Column being compared (for value context) */
  columnName?: string;
  /** Table alias for the column (for value context) */
  columnTableAlias?: string;
}

/**
 * Extract column name from text before a comparison operator
 * Handles: "column =", "table.column =", "column IN", "column = 'partial", etc.
 */
function extractColumnBeforeOperator(
  textBefore: string
): { columnName: string; tableAlias?: string } | null {
  // Match patterns like: column =, column !=, column IN, table.column =, etc.
  // We need to capture the column (and optional table prefix) before the operator
  // Also match when user is typing a partial string value like: column = 'val
  const patterns = [
    // column = or column != or column <> (with optional whitespace and optional partial string value)
    /(\w+)\.(\w+)\s*(?:=|!=|<>)\s*(?:'[^']*)?$/i,
    /(\w+)\s*(?:=|!=|<>)\s*(?:'[^']*)?$/i,
    // column IN ( or column NOT IN ( (with optional partial string value)
    /(\w+)\.(\w+)\s+(?:NOT\s+)?IN\s*\(\s*(?:'[^']*)?$/i,
    /(\w+)\s+(?:NOT\s+)?IN\s*\(\s*(?:'[^']*)?$/i,
    // After a comma in IN clause (with optional partial string value)
    /(\w+)\.(\w+)\s+(?:NOT\s+)?IN\s*\([^)]*,\s*(?:'[^']*)?$/i,
    /(\w+)\s+(?:NOT\s+)?IN\s*\([^)]*,\s*(?:'[^']*)?$/i,
  ];

  for (const pattern of patterns) {
    const match = textBefore.match(pattern);
    if (match) {
      if (match.length === 3) {
        // table.column pattern
        return { tableAlias: match[1], columnName: match[2] };
      } else {
        // just column pattern
        return { columnName: match[1] };
      }
    }
  }

  return null;
}

function determineContext(doc: string, pos: number): ContextResult {
  // Get text before cursor
  const textBefore = doc.slice(0, pos);

  // Check if we're in a value context (after comparison operator)
  // This should be checked before other contexts
  const columnInfo = extractColumnBeforeOperator(textBefore);
  if (columnInfo) {
    return {
      type: "value",
      columnName: columnInfo.columnName,
      columnTableAlias: columnInfo.tableAlias,
    };
  }

  // Check if we're completing after a dot (table.column)
  const dotMatch = textBefore.match(/(\w+)\.\s*$/);
  if (dotMatch) {
    return { type: "alias", tablePrefix: dotMatch[1] };
  }

  // Find the LAST significant keyword before cursor
  // We match all keywords and take the last one
  const keywordPattern = /\b(SELECT|FROM|JOIN|WHERE|AND|OR|ORDER\s+BY|GROUP\s+BY|HAVING|ON)\b/gi;
  let lastMatch: RegExpExecArray | null = null;
  let match: RegExpExecArray | null;

  while ((match = keywordPattern.exec(textBefore)) !== null) {
    lastMatch = match;
  }

  if (lastMatch) {
    const keyword = lastMatch[1].toUpperCase().replace(/\s+/g, " ");

    if (keyword === "FROM" || keyword === "JOIN") {
      return { type: "table" };
    }

    if (
      keyword === "SELECT" ||
      keyword === "WHERE" ||
      keyword === "AND" ||
      keyword === "OR" ||
      keyword === "ORDER BY" ||
      keyword === "GROUP BY" ||
      keyword === "HAVING" ||
      keyword === "ON"
    ) {
      return { type: "column" };
    }
  }

  return { type: "general" };
}

/**
 * Find a column schema by name in the tables map
 */
function findColumnSchema(
  columnName: string,
  tableAlias: string | undefined,
  tables: Map<string, TableSchema>
): ColumnSchema | null {
  if (tableAlias) {
    // Look in specific table
    const tableSchema = tables.get(tableAlias.toLowerCase());
    if (tableSchema) {
      return tableSchema.columns[columnName] || null;
    }
  } else {
    // Look in all tables
    for (const tableSchema of tables.values()) {
      const col = tableSchema.columns[columnName];
      if (col) {
        return col;
      }
    }
  }
  return null;
}

/**
 * Create completions for enum values from allowedValues
 */
function createEnumValueCompletions(columnSchema: ColumnSchema): Completion[] {
  if (!columnSchema.allowedValues || columnSchema.allowedValues.length === 0) {
    return [];
  }

  return columnSchema.allowedValues.map((value) => ({
    label: `'${value}'`,
    type: "enum",
    detail: columnSchema.description || "allowed value",
    boost: 3, // Highest priority for enum values in value context
  }));
}

/**
 * Create a TSQL-aware autocompletion source
 *
 * @param schema - Array of table schemas to use for completions
 * @returns A CodeMirror completion source function
 */
export function createTSQLCompletion(
  schema: TableSchema[]
): (context: CompletionContext) => CompletionResult | null {
  // Pre-compute static completions
  const keywordCompletions = createKeywordCompletions();
  const functionCompletions = createFunctionCompletions();
  const tableCompletions = createTableCompletions(schema);

  return (context: CompletionContext): CompletionResult | null => {
    // Get the word being typed - include single quotes for value completion
    const word = context.matchBefore(/[\w.']+/);

    // Don't show completions if no word is being typed and not explicitly triggered
    if (!word && !context.explicit) {
      return null;
    }

    const from = word ? word.from : context.pos;
    const doc = context.state.doc.toString();
    const queryContext = determineContext(doc, context.pos);

    let options: Completion[] = [];
    // Track if we need to extend replacement range (e.g., to consume auto-paired closing quote)
    let to: number | undefined = undefined;

    switch (queryContext.type) {
      case "table":
        // After FROM or JOIN, show only tables
        options = tableCompletions;
        break;

      case "alias":
        // After table., show columns for that table
        if (queryContext.tablePrefix) {
          const tables = extractTablesFromQuery(doc, schema);
          const tableSchema = tables.get(queryContext.tablePrefix.toLowerCase());

          if (tableSchema) {
            options = createColumnCompletions(tableSchema);
          }
        }
        break;

      case "value":
        // After comparison operator, show enum values if available
        if (queryContext.columnName) {
          const tables = extractTablesFromQuery(doc, schema);
          const columnSchema = findColumnSchema(
            queryContext.columnName,
            queryContext.columnTableAlias,
            tables
          );

          if (columnSchema) {
            options = createEnumValueCompletions(columnSchema);
            // Check if there's a closing quote right after cursor (from auto-pairing)
            // If so, extend replacement range to include it to avoid 'Completed''
            const charAfterCursor = context.state.doc.sliceString(context.pos, context.pos + 1);
            if (charAfterCursor === "'") {
              to = context.pos + 1;
            }
          }
        }
        break;

      case "column":
        // After SELECT, WHERE, etc., show columns, functions, and some keywords
        {
          const tables = extractTablesFromQuery(doc, schema);

          // Add columns from all tables in the query
          tables.forEach((tableSchema, alias) => {
            // If multiple tables, prefix with alias
            const prefix = tables.size > 1 ? alias : undefined;
            options.push(...createColumnCompletions(tableSchema, prefix));
          });

          // Also add functions and relevant keywords
          options.push(...functionCompletions);
          options.push(
            ...keywordCompletions.filter((k) =>
              [
                "AND",
                "OR",
                "NOT",
                "IN",
                "LIKE",
                "ILIKE",
                "BETWEEN",
                "IS",
                "NULL",
                "AS",
                "CASE",
                "WHEN",
                "THEN",
                "ELSE",
                "END",
              ].includes(k.label as string)
            )
          );
        }
        break;

      case "general":
      default:
        // Show everything
        options = [...tableCompletions, ...functionCompletions, ...keywordCompletions];

        // Also add columns from tables in query
        {
          const tables = extractTablesFromQuery(doc, schema);
          tables.forEach((tableSchema, alias) => {
            const prefix = tables.size > 1 ? alias : undefined;
            options.push(...createColumnCompletions(tableSchema, prefix));
          });
        }
        break;
    }

    const result: CompletionResult = {
      from,
      options,
      validFor: /^[\w.']*$/,
    };
    // Only set 'to' if we need to extend the replacement range
    if (to !== undefined) {
      result.to = to;
    }
    return result;
  };
}
