import type { ReactNode } from 'react';

type TokenKind =
  | 'plain'
  | 'keyword'
  | 'string'
  | 'number'
  | 'boolean'
  | 'null'
  | 'property'
  | 'comment'
  | 'operator'
  | 'punctuation'
  | 'tag'
  | 'attribute'
  | 'selector'
  | 'command'
  | 'path'
  | 'option';

type Token = {
  kind: TokenKind;
  value: string;
};

type EditorLineTone = 'default' | 'diff-added' | 'diff-removed' | 'diff-hunk';

function tokenizeJson(input: string): Token[] {
  const pattern =
    /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\],:]/g;
  const tokens: Token[] = [];
  let lastIndex = 0;

  input.replace(pattern, (match, quoted, colon, booleanValue, offset) => {
    if (offset > lastIndex) {
      tokens.push({ kind: 'plain', value: input.slice(lastIndex, offset) });
    }

    if (quoted) {
      tokens.push({ kind: colon ? 'property' : 'string', value: quoted });
      if (colon) {
        tokens.push({ kind: 'punctuation', value: ':' });
      }
    } else if (booleanValue) {
      tokens.push({ kind: 'boolean', value: match });
    } else if (match === 'null') {
      tokens.push({ kind: 'null', value: match });
    } else if (/^-?\d/.test(match)) {
      tokens.push({ kind: 'number', value: match });
    } else {
      tokens.push({ kind: 'punctuation', value: match });
    }

    lastIndex = offset + match.length;
    return match;
  });

  if (lastIndex < input.length) {
    tokens.push({ kind: 'plain', value: input.slice(lastIndex) });
  }

  return tokens;
}

function tokenizeShell(input: string): Token[] {
  const trimmed = input.trimStart();

  if (!trimmed) {
    return [];
  }

  if (trimmed.startsWith('#')) {
    return [{ kind: 'comment', value: input }];
  }

  const promptMatch = input.match(/^(\s*[$>#]\s+)/);
  const prompt = promptMatch?.[1] ?? '';
  const body = prompt ? input.slice(prompt.length) : input;
  const bodyMatch = body.match(/^([A-Za-z0-9_./:-]+)(.*)$/);

  if (!bodyMatch) {
    return [{ kind: 'plain', value: input }];
  }

  const [, command, rest] = bodyMatch;
  const tokens: Token[] = [];

  if (prompt) {
    tokens.push({ kind: 'operator', value: prompt });
  }

  tokens.push({ kind: 'command', value: command });

  if (!rest) {
    return tokens;
  }

  const parts = rest.split(/(\s+)/);

  parts.forEach((part) => {
    if (!part) {
      return;
    }

    if (/^\s+$/.test(part)) {
      tokens.push({ kind: 'plain', value: part });
      return;
    }

    if (/^--?[A-Za-z0-9][A-Za-z0-9-]*$/.test(part)) {
      tokens.push({ kind: 'option', value: part });
      return;
    }

    if (/^(["'`]).*\1$/.test(part)) {
      tokens.push({ kind: 'string', value: part });
      return;
    }

    if (/^-?\d+(?:\.\d+)?$/.test(part)) {
      tokens.push({ kind: 'number', value: part });
      return;
    }

    if (/^(?:[./~]|[A-Za-z]:\\|\/)[^\s]*$/.test(part) || /^[\w.-]+\/[^\s]+$/.test(part)) {
      tokens.push({ kind: 'path', value: part });
      return;
    }

    if (/^(?:\|\||&&|[|<>])$/.test(part)) {
      tokens.push({ kind: 'operator', value: part });
      return;
    }

    tokens.push({ kind: 'plain', value: part });
  });

  return tokens;
}

function tokenizePathLine(input: string): Token[] {
  if (!input.trim()) {
    return [];
  }

  const bulletMatch = input.match(/^(\s*[-*]\s+)(.*)$/);

  if (bulletMatch) {
    return [
      { kind: 'operator', value: bulletMatch[1] },
      { kind: 'path', value: bulletMatch[2] },
    ];
  }

  return [{ kind: 'path', value: input }];
}

function tokenizeGenericCode(input: string, language: string): Token[] {
  const commentPattern =
    language === 'html'
      ? /<!--[\s\S]*?-->/y
      : language === 'python'
        ? /#.*|""".*?"""|'''.*?'''/y
        : /\/\/.*|\/\*[\s\S]*?\*\//y;
  const stringPattern = /(["'`])(?:\\.|(?!\1)[\s\S])*\1/y;
  const numberPattern = /\b\d+(?:\.\d+)?\b/y;
  const keywordPattern =
    /\b(const|let|var|function|return|if|else|for|while|switch|case|break|continue|import|from|export|default|async|await|type|interface|extends|implements|new|class|try|catch|finally|throw|public|private|protected|true|false|null|undefined|select|from|where|order|by|group|insert|into|update|delete|join|left|right|inner|outer|limit|and|or|not|as|if|then|fi|do|done|export|set)\b/y;
  const tagPattern = /<\/?[A-Za-z][^>\s/]*/y;
  const attributePattern = /\b[A-Za-z-:]+(?==)/y;
  const selectorPattern = /[.#]?[A-Za-z_][A-Za-z0-9_-]*(?=\s*\{)/y;
  const operatorPattern = /=>|===|!==|==|!=|<=|>=|\+\+|--|&&|\|\||[=+\-*/%<>!&|?:]/y;
  const punctuationPattern = /[()[\]{}.,;]/y;
  const plainPattern = /\s+|[A-Za-z_$][\w$-]*/y;

  const tokens: Token[] = [];
  let index = 0;

  while (index < input.length) {
    const sliceIndex = index;
    const patterns: Array<[RegExp, TokenKind]> = [
      [commentPattern, 'comment'],
      [stringPattern, 'string'],
      [numberPattern, 'number'],
      [keywordPattern, language === 'css' ? 'selector' : 'keyword'],
      [tagPattern, 'tag'],
      [attributePattern, 'attribute'],
      [selectorPattern, 'selector'],
      [operatorPattern, 'operator'],
      [punctuationPattern, 'punctuation'],
      [plainPattern, 'plain'],
    ];

    let matched = false;

    for (const [pattern, kind] of patterns) {
      pattern.lastIndex = sliceIndex;
      const result = pattern.exec(input);

      if (result && result.index === sliceIndex) {
        tokens.push({ kind, value: result[0] });
        index += result[0].length;
        matched = true;
        break;
      }
    }

    if (!matched) {
      tokens.push({ kind: 'plain', value: input[index] });
      index += 1;
    }
  }

  return tokens;
}

export function inferCodeLanguage(language: string | undefined) {
  const normalized = (language ?? 'text').toLowerCase();

  if (normalized === 'sh' || normalized === 'shell') {
    return 'bash';
  }

  if (normalized === 'ts') {
    return 'typescript';
  }

  if (normalized === 'js') {
    return 'javascript';
  }

  if (normalized === 'md') {
    return 'markdown';
  }

  return normalized;
}

function tokenizeCode(input: string, language: string): Token[] {
  if (language === 'json') {
    return tokenizeJson(input);
  }

  if (language === 'bash' || language === 'zsh') {
    return tokenizeShell(input);
  }

  if (language === 'paths') {
    return tokenizePathLine(input);
  }

  return tokenizeGenericCode(input, language);
}

function getLineTone(line: string, language: string): EditorLineTone {
  if (language !== 'diff') {
    return 'default';
  }

  if (line.startsWith('@@')) {
    return 'diff-hunk';
  }

  if (line.startsWith('+')) {
    return 'diff-added';
  }

  if (line.startsWith('-')) {
    return 'diff-removed';
  }

  return 'default';
}

function renderTokens(tokens: Token[]) {
  return tokens.map((token, index) => (
    <span key={`${token.kind}-${index}`} className={`previewer-ui__token previewer-ui__token--${token.kind}`}>
      {token.value}
    </span>
  ));
}

export function renderEditorBlock(value: string, language: string, mode: 'code' | 'json'): ReactNode {
  const normalizedLanguage = mode === 'json' ? 'json' : inferCodeLanguage(language);
  const lines = value.split('\n');

  return (
    <div className="previewer-ui__editor">
      <div className="previewer-ui__editor-bar">
        <div className="previewer-ui__editor-dots">
          <span />
          <span />
          <span />
        </div>
        <div className="previewer-ui__editor-tab">{normalizedLanguage.toUpperCase()}</div>
      </div>
      <div className="previewer-ui__editor-body">
        {lines.map((line, lineIndex) => {
          const tokens = tokenizeCode(line, normalizedLanguage);
          const lineTone = getLineTone(line, normalizedLanguage);

          return (
            <div
              key={`line-${lineIndex}`}
              className={`previewer-ui__line${lineTone !== 'default' ? ` previewer-ui__line--${lineTone}` : ''}`}
            >
              <span className="previewer-ui__line-number">{lineIndex + 1}</span>
              <code className="previewer-ui__line-code">{tokens.length === 0 ? ' ' : renderTokens(tokens)}</code>
            </div>
          );
        })}
      </div>
    </div>
  );
}
