export interface SqlStatementRange {
  start: number;
  end: number;
  text: string;
}

export type SqlExecutionSelectionSource = 'selection' | 'statement' | 'line';

export interface SqlExecutionSelection {
  sql: string;
  source: SqlExecutionSelectionSource;
}

const isWhitespace = (ch: string): boolean => (
  ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f'
);

const isSqlIdentifierStart = (ch: string): boolean => /^[A-Za-z_]$/.test(ch);

const isSqlIdentifierPart = (ch: string): boolean => /^[A-Za-z0-9_$#]$/.test(ch);

const skipSqlWhitespaceAndComments = (text: string, position: number): number => {
  let index = position;
  while (index < text.length) {
    const ch = text[index];
    const next = index + 1 < text.length ? text[index + 1] : '';
    if (isWhitespace(ch)) {
      index += 1;
      continue;
    }
    if (ch === '-' && next === '-') {
      index += 2;
      while (index < text.length && text[index] !== '\n') index += 1;
      continue;
    }
    if (ch === '/' && next === '*') {
      index += 2;
      while (index + 1 < text.length && !(text[index] === '*' && text[index + 1] === '/')) {
        index += 1;
      }
      if (index + 1 < text.length) index += 2;
      continue;
    }
    break;
  }
  return index;
};

const nextSqlSignificantToken = (text: string, position: number): string => {
  const index = skipSqlWhitespaceAndComments(text, position);
  if (index >= text.length || !isSqlIdentifierStart(text[index])) return '';
  let end = index + 1;
  while (end < text.length && isSqlIdentifierPart(text[end])) end += 1;
  return text.slice(index, end).toLowerCase();
};

const nextSqlSignificantChar = (text: string, position: number): string => {
  const index = skipSqlWhitespaceAndComments(text, position);
  return index >= text.length ? '' : text[index];
};

const shouldEnterPlsqlBeginBlock = (text: string, tokenEnd: number): boolean => {
  const nextChar = nextSqlSignificantChar(text, tokenEnd);
  if (!nextChar || nextChar === ';') return false;
  return !['transaction', 'work', 'isolation', 'read', 'write'].includes(nextSqlSignificantToken(text, tokenEnd));
};

const shouldEnterPlsqlDeclareBlock = (text: string, tokenEnd: number): boolean => Boolean(nextSqlSignificantToken(text, tokenEnd));

const nextSqlSignificantTokenSpan = (text: string, position: number): { token: string; end: number } => {
  const index = skipSqlWhitespaceAndComments(text, position);
  if (index >= text.length || !isSqlIdentifierStart(text[index])) {
    return { token: '', end: index };
  }
  let end = index + 1;
  while (end < text.length && isSqlIdentifierPart(text[end])) end += 1;
  return { token: text.slice(index, end).toLowerCase(), end };
};

const isCreateRoutineHeaderPrefix = (text: string): boolean => {
  let current = nextSqlSignificantTokenSpan(text, 0);
  if (current.token !== 'create') return false;

  current = nextSqlSignificantTokenSpan(text, current.end);
  if (current.token === 'or') {
    current = nextSqlSignificantTokenSpan(text, current.end);
    if (current.token !== 'replace') return false;
    current = nextSqlSignificantTokenSpan(text, current.end);
  }

  while (['editionable', 'noneditionable'].includes(current.token)) {
    current = nextSqlSignificantTokenSpan(text, current.end);
  }

  return current.token === 'procedure' || current.token === 'function';
};

const shouldEnterPlsqlCreateRoutineBlock = (
  text: string,
  statementStart: number,
  token: string,
  tokenEnd: number,
): boolean => {
  if (token !== 'is' && token !== 'as') return false;
  const nextChar = nextSqlSignificantChar(text, tokenEnd);
  if (!nextChar) return false;
  if (token === 'as' && (nextChar === '$' || nextChar === "'" || nextChar === '"')) {
    return false;
  }
  return isCreateRoutineHeaderPrefix(text.slice(statementStart, tokenEnd - token.length));
};

const isPlsqlControlEnd = (text: string, tokenEnd: number): boolean => (
  ['if', 'loop', 'case'].includes(nextSqlSignificantToken(text, tokenEnd))
);

const trimStatementRange = (sql: string, start: number, end: number): SqlStatementRange | null => {
  let nextStart = Math.max(0, start);
  let nextEnd = Math.min(sql.length, Math.max(start, end));

  while (nextStart < nextEnd && isWhitespace(sql[nextStart])) {
    nextStart++;
  }
  while (nextEnd > nextStart && isWhitespace(sql[nextEnd - 1])) {
    nextEnd--;
  }

  if (nextStart >= nextEnd) {
    return null;
  }

  return {
    start: nextStart,
    end: nextEnd,
    text: sql.slice(nextStart, nextEnd),
  };
};

export const findSqlStatementRanges = (sql: string): SqlStatementRange[] => {
  const text = String(sql || '').replace(/\r\n/g, '\n');
  const ranges: SqlStatementRange[] = [];

  let statementStart = 0;
  let inSingle = false;
  let inDouble = false;
  let inBacktick = false;
  let escaped = false;
  let inLineComment = false;
  let inBlockComment = false;
  let dollarTag: string | null = null;
  let plsqlDepth = 0;
  let plsqlDeclareBeginSkips = 0;
  let justClosedPLSQLBlock = false;

  const push = (end: number) => {
    const range = trimStatementRange(text, statementStart, end);
    if (range) {
      ranges.push(range);
    }
  };

  for (let index = 0; index < text.length; index++) {
    const ch = text[index];
    const next = index + 1 < text.length ? text[index + 1] : '';
    const prev = index > 0 ? text[index - 1] : '';
    const next2 = index + 2 < text.length ? text[index + 2] : '';

    if (dollarTag) {
      if (text.startsWith(dollarTag, index)) {
        index += dollarTag.length - 1;
        dollarTag = null;
      }
      continue;
    }

    if (inLineComment) {
      if (ch === '\n') {
        inLineComment = false;
      }
      continue;
    }

    if (inBlockComment) {
      if (ch === '*' && next === '/') {
        index++;
        inBlockComment = false;
      }
      continue;
    }

    if (!inSingle && !inDouble && !inBacktick) {
      if (ch === '/' && next === '*') {
        index++;
        inBlockComment = true;
        continue;
      }
      if (ch === '#') {
        inLineComment = true;
        continue;
      }
      if (ch === '-' && next === '-' && (index === 0 || isWhitespace(prev)) && (next2 === '' || isWhitespace(next2))) {
        index++;
        inLineComment = true;
        continue;
      }
      if (ch === '$') {
        const match = text.slice(index).match(/^\$[A-Za-z0-9_]*\$/);
        if (match?.[0]) {
          dollarTag = match[0];
          index += dollarTag.length - 1;
          continue;
        }
      }
    }

    if (escaped) {
      escaped = false;
      continue;
    }

    if ((inSingle || inDouble) && ch === '\\') {
      escaped = true;
      continue;
    }

    if (!inDouble && !inBacktick && ch === "'") {
      inSingle = !inSingle;
      continue;
    }
    if (!inSingle && !inBacktick && ch === '"') {
      inDouble = !inDouble;
      continue;
    }
    if (!inSingle && !inDouble && ch === '`') {
      inBacktick = !inBacktick;
      continue;
    }

    if (!inSingle && !inDouble && !inBacktick && !dollarTag && isSqlIdentifierStart(ch)) {
      let tokenEnd = index + 1;
      while (tokenEnd < text.length && isSqlIdentifierPart(text[tokenEnd])) {
        tokenEnd++;
      }
      const token = text.slice(index, tokenEnd).toLowerCase();
      if (token === 'begin' && plsqlDeclareBeginSkips > 0) {
        plsqlDeclareBeginSkips--;
        justClosedPLSQLBlock = false;
      } else if (token === 'begin' && shouldEnterPlsqlBeginBlock(text, tokenEnd)) {
        plsqlDepth++;
        justClosedPLSQLBlock = false;
      } else if (token === 'declare' && shouldEnterPlsqlDeclareBlock(text, tokenEnd)) {
        plsqlDepth++;
        plsqlDeclareBeginSkips++;
        justClosedPLSQLBlock = false;
      } else if (plsqlDepth === 0 && shouldEnterPlsqlCreateRoutineBlock(text, statementStart, token, tokenEnd)) {
        plsqlDepth++;
        plsqlDeclareBeginSkips++;
        justClosedPLSQLBlock = false;
      } else if (token === 'end' && plsqlDepth > 0 && !isPlsqlControlEnd(text, tokenEnd)) {
        plsqlDepth--;
        if (plsqlDeclareBeginSkips > plsqlDepth) {
          plsqlDeclareBeginSkips = plsqlDepth;
        }
        justClosedPLSQLBlock = plsqlDepth === 0;
      }
      index = tokenEnd - 1;
      continue;
    }

    if (!inSingle && !inDouble && !inBacktick && (ch === ';' || ch === '；')) {
      if (plsqlDepth > 0) {
        continue;
      }
      push(justClosedPLSQLBlock ? index + 1 : index);
      statementStart = index + 1;
      justClosedPLSQLBlock = false;
      continue;
    }
  }

  push(text.length);
  return ranges;
};

export const resolveCurrentSqlStatementRange = (sql: string, cursorOffset: number): SqlStatementRange | null => {
  const text = String(sql || '').replace(/\r\n/g, '\n');
  const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
  const ranges = findSqlStatementRanges(text);
  if (ranges.length === 0) {
    return null;
  }

  const containingRange = ranges.find((range) => offset >= range.start && offset <= range.end);
  if (containingRange) {
    return containingRange;
  }

  const nextRange = ranges.find((range) => offset < range.start);
  if (nextRange) {
    return nextRange;
  }

  return ranges[ranges.length - 1];
};

export const resolveExecutableSql = (
  sql: string,
  cursorOffset: number,
  selectedSql = '',
): SqlExecutionSelection | null => {
  const selected = String(selectedSql || '').trim();
  if (selected) {
    return { sql: selectedSql, source: 'selection' };
  }

  const text = String(sql || '').replace(/\r\n/g, '\n');
  const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
  const ranges = findSqlStatementRanges(text);
  const statement = ranges.find((range) => offset >= range.start && offset <= range.end);
  if (statement?.text.trim()) {
    return { sql: statement.text, source: 'statement' };
  }

  const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
  const nextLineBreak = text.indexOf('\n', offset);
  const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak;
  const line = text.slice(lineStart, lineEnd).trim();
  if (line) {
    const lineStatement = [...ranges].reverse().find((range) => range.start < lineEnd && range.end >= lineStart);
    if (lineStatement?.text.trim()) {
      return { sql: lineStatement.text, source: 'statement' };
    }
  }
  if (line) {
    return { sql: line, source: 'line' };
  }

  return null;
};
