import random
import re

from sqlite_static_helper import *

_TOKEN_RE = re.compile(
  r"""
    (?P<line_comment>--[^\n]*)                            |
    (?P<block_comment>/\*.*?\*/)                          |
    (?P<string>'(?:[^']|'')*')                            |
    (?P<dquoted>"(?:[^"]|"")*")                           |
    (?P<bracket>\[[^\]]*\])                               |
    (?P<backtick>`(?:[^`]|``)*`)                          |
    (?P<blob>[xX]'[0-9a-fA-F]*')                          |
    (?P<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)        |
    (?P<ident>[A-Za-z_][A-Za-z0-9_]*)                     |
    (?P<op>[<>!=]=|<>|\|\||::|->>?|[+\-*/%<>=&|^~,.;()@]) |
    (?P<ws>\s+)                                           |
    (?P<other>.)
  """,
  re.VERBOSE | re.DOTALL,
)

_UNSAFE_KINDS = {
  "line_comment", "block_comment", "string", "dquoted", "bracket", "backtick",
  "blob"
}


def _tokenize(s: str):
  return [(m.lastgroup, m.group()) for m in _TOKEN_RE.finditer(s)]


def _sub_in_safe(s: str, pattern: re.Pattern, repl, max_subs: int = 1) -> str:
  """Run `pattern.sub(repl, …)` only outside strings/comments/blob literals."""
  if max_subs <= 0:
    return s
  parts: list[str] = []
  done = 0
  for kind, text in _tokenize(s):
    if done >= max_subs or kind in _UNSAFE_KINDS:
      parts.append(text)
      continue
    new_text, n = pattern.subn(repl, text, count=max_subs - done)
    parts.append(new_text)
    done += n
  return ''.join(parts)


_CMP_RE = re.compile(r'(?<![<>!=])(<=|>=|<>|!=|==|=|<|>)(?!=)')


def mut_swap_comparison(s: str) -> str:
  """Swap the left-most comparison operator with a different one"""

  def repl(m: re.Match) -> str:
    op = m.group(1)
    return random.choice([o for o in CMP_OPS if o != op])

  return _sub_in_safe(s, _CMP_RE, repl, 1)


_BOOL_RE = re.compile(r'\b(AND|OR)\b', re.I)


def mut_swap_boolean(s: str) -> str:
  """Swap the left-most boolean operator with a different one"""

  def repl(m: re.Match) -> str:
    return 'OR' if m.group(1).upper() == 'AND' else 'AND'

  return _sub_in_safe(s, _BOOL_RE, repl, 1)


def mut_negate_where(s: str) -> str:
  """Swap the left-most WHERE with WHERE NOT"""
  if not re.search(r'\bWHERE\b', s, re.I):
    return s
  return _sub_in_safe(s, re.compile(r'\bWHERE\b', re.I), 'WHERE NOT', 1)


def mut_change_type(s: str) -> str:
  """Swap the left-most data type with a different one"""
  pat = re.compile(
    r'\b(' + '|'.join(
      re.escape(t)
      for t in sorted(TYPES, key=len, reverse=True)) +
    r')\b',
    re.I,
  )

  def repl(m: re.Match) -> str:
    cur = m.group(1).upper()
    return random.choice(
      [t for t in TYPES if t.upper() != cur])

  return _sub_in_safe(s, pat, repl, 1)

def mut_swap_join_type(s: str) -> str:
  """Swap one join type to a different join type"""

  join_pattern = '|'.join(
    re.escape(j) for j in sorted(JOINS, key=len, reverse=True))
  pat = re.compile(rf'\b({join_pattern})\b', re.I)

  m = pat.search(s)
  if not m:
    return s

  matched = m.group(1).upper()
  alternatives = [j for j in JOINS if j.upper() != matched]
  if not alternatives:
    return s

  replacement = random.choice(alternatives)
  if m.group(1).isupper():
    replacement = replacement.upper()
  elif m.group(1).islower():
    replacement = replacement.lower()
  elif m.group(1)[0].isupper() and m.group(1)[1:].islower():
    replacement = replacement.title()

  return s[:m.start()] + replacement + s[m.end():]
