export type Value = string | number | null | Sql | Ident;

/**
 * Quotes the value to be used as a shell argument.
 */
export function shellQuote(s: string): string {
  function quote(s: string): string {
    return "'" + s.replace(/'/g, "'\\''") + "'";
  }

  return s === ";"
    ? "\\;"
    : s.match(/^[-a-z_0-9:/@.=,]+$/is)
      ? s
      : s.match(/^((?:--)?[-a-z_0-9]+=)(.*)$/is)
        ? RegExp.$1 + quote(RegExp.$2)
        : quote(s);
}

/**
 * Builds a compound SQL statement from a typed template string literal. All SQL
 * clauses built by this helper will have newlines removed.
 */
export function sql(
  strings: readonly string[],
  ...values: readonly Value[]
): Sql {
  const SEP = "\x00";
  return new Sql(
    strings
      .join(SEP)
      .trim()
      .replace(/[ \t\r\n]+/g, " ") // glue all lines in one
      .trimEnd()
      .split(SEP),
    values,
  );
}

/**
 * Builds a PG identifier from a string.
 */
export function ident(name: string): Ident {
  return new Ident(name);
}

/**
 * Joins multiple compound SQL statements.
 */
export function join(
  sqls: ReadonlyArray<Sql | Ident | string>,
  sep: string,
): Sql {
  return new Sql([sqls.map((sql) => sql.toString()).join(sep)], []);
}

class Ident {
  constructor(private name: string) {}

  toString(): string {
    return this.name.match(/^[a-z_][a-z_0-9]*$/is)
      ? this.name
      : '"' + this.name.replace(/"/g, '""') + '"';
  }
}

export class Sql {
  constructor(
    private strings: readonly string[],
    private values: readonly Value[],
  ) {}

  toString(): string {
    return (
      this.values
        .map(
          (value, i) =>
            this.strings[i] +
            (value instanceof Sql || value instanceof Ident
              ? value.toString()
              : value === null || value === undefined
                ? "NULL"
                : // Postgres doesn't like ASCII NUL character (error message is "unterminated
                  // quoted string" or "invalid message format"), so we remove it too.
                  "'" +
                  ("" + value).replace(/\0/g, "").replace(/'/g, "''") +
                  "'"),
        )
        .join("") + this.strings[this.values.length]
    );
  }
}
