/**
 * Preprocesses BILDIT template code for JS/JSX/TS/TSX transpilation.
 * Replaces $(varName:Type="default") with the default value, and $(varName:Type) with null,
 * so the code can be parsed by Babel and validated before adding to Templates.
 *
 * Uses @bildit-platform/template-parser when possible; falls back to regex for edge cases.
 */

const TEMPLATE_VAR_REGEX =
  /\$\(([\w$]+):(\w+)(?:\[([^\]]*)\])?(?:=(?:(?<q>['"`])([^\n]*?)\k<q>|([^:)]*?)))?(?::([^)]*?))?\)/g

/**
 * Rewrites mistaken `$((name as Type) = default)` (TS cast inside double `(`) to BILD v1
 * `$(name:Type=default)` so lint, template-parser, and the CMS see valid variables.
 */
function consumeTemplateDefaultLiteral(code, pos) {
  if (pos >= code.length) return null
  const c = code[pos]
  if (c === '{') {
    // Consume a JS object literal (used by older RichText defaults: { text: "..." }).
    // We only need a best-effort balanced-brace scan that respects strings/escapes.
    let j = pos
    let depth = 0
    let inStr = null
    while (j < code.length) {
      const ch = code[j]
      if (inStr) {
        if (ch === '\\') {
          j += 2
          continue
        }
        if (ch === inStr) {
          inStr = null
          j += 1
          continue
        }
        j += 1
        continue
      }
      if (ch === '"' || ch === "'" || ch === '`') {
        inStr = ch
        j += 1
        continue
      }
      if (ch === '{') {
        depth += 1
        j += 1
        continue
      }
      if (ch === '}') {
        depth -= 1
        j += 1
        if (depth === 0) {
          const raw = code.slice(pos, j)
          return { inner: raw, raw, end: j }
        }
        continue
      }
      j += 1
    }
    return null
  }
  if (c === "'" || c === '"') {
    const q = c
    let j = pos + 1
    while (j < code.length) {
      const ch = code[j]
      if (ch === '\\') {
        j += 2
        continue
      }
      if (ch === q) {
        return {
          inner: code.slice(pos + 1, j),
          raw: code.slice(pos, j + 1),
          end: j + 1,
        }
      }
      j += 1
    }
    return null
  }
  const tail = code.slice(pos)
  const boolM = /^(true|false)\b/.exec(tail)
  if (boolM) {
    return { inner: boolM[1], raw: boolM[0], end: pos + boolM[0].length }
  }
  const numM = /^-?\d+(?:\.\d+)?/.exec(tail)
  if (numM) {
    return { inner: numM[0], raw: numM[0], end: pos + numM[0].length }
  }
  return null
}

function formatBildDefaultSuffix(fieldType, def) {
  if (fieldType === 'Boolean' || fieldType === 'Number') {
    return `=${def.raw}`
  }
  if (fieldType === 'RichText') {
    // If the default already looks like an object literal, keep it as-is.
    if (def.raw && String(def.raw).trim().startsWith('{')) {
      return `=${def.raw}`
    }
    const inner = def.inner.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
    return `={ text: "${inner}" }`
  }
  if (fieldType === 'Image') {
    // Image keeps existing object literal defaults verbatim; bare string defaults become
    // `{ url: <string>, alt: "" }` so consumers can always read `.url` and `.alt`.
    if (def.raw && String(def.raw).trim().startsWith('{')) {
      return `=${def.raw}`
    }
    const inner = def.inner.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
    return `={ url: "${inner}", alt: "" }`
  }
  const inner = def.inner.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
  return `='${inner}'`
}

/**
 * @param {string} code
 * @param {number} start - index of '$'
 * @returns {{ replacement: string, end: number } | null}
 */
function tryConsumeBrokenDoubleParenTemplate(code, start) {
  if (code[start] !== '$' || code[start + 1] !== '(' || code[start + 2] !== '(') {
    return null
  }
  let pos = start + 3
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  const idM = /^([a-zA-Z_$][\w$]*)\b/.exec(code.slice(pos))
  if (!idM) return null
  const id = idM[1]
  pos += idM[0].length
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  const asM = /^\bas\b/.exec(code.slice(pos))
  if (!asM) return null
  pos += asM[0].length
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  const typeM = /^(String|Boolean|Number|RichText|Color|Image|Link)\b/.exec(code.slice(pos))
  if (!typeM) return null
  const fieldType = typeM[1]
  pos += typeM[0].length
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  if (code[pos] !== ')') return null
  pos += 1
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  let defaultSuffix = ''
  if (code[pos] === '=') {
    pos += 1
    while (pos < code.length && /\s/.test(code[pos])) pos += 1
    const def = consumeTemplateDefaultLiteral(code, pos)
    if (!def) return null
    defaultSuffix = formatBildDefaultSuffix(fieldType, def)
    pos = def.end
  }
  while (pos < code.length && /\s/.test(code[pos])) pos += 1
  if (code[pos] !== ')') return null
  pos += 1
  const replacement = defaultSuffix ? `$(${id}:${fieldType}${defaultSuffix})` : `$(${id}:${fieldType})`
  return { replacement, end: pos }
}

function canonicalizeDoubleParenAsCastTemplates(code, codeType) {
  if (!code || typeof code !== 'string') return code
  if (codeType === 'html') return code
  if (!code.includes('$((')) return code
  let out = ''
  let i = 0
  while (i < code.length) {
    if (code[i] === '$' && code[i + 1] === '(' && code[i + 2] === '(') {
      const hit = tryConsumeBrokenDoubleParenTemplate(code, i)
      if (hit) {
        out += hit.replacement
        i = hit.end
        continue
      }
    }
    out += code[i]
    i += 1
  }
  return out
}

/**
 * Produces a JS object literal for RichText so `field.text` works after substitution.
 * String defaults from the grammar (`$(a:RichText="x")`) are parsed as strings; we wrap
 * them as `{ text: "x" }`. For CMS field metadata, prefer `parseTemplateMetadata` (Lezer)
 * over regex on `$(name:RichText=...)`.
 * @param {unknown} resolved
 */
function serializeRichTextDefaultForPreprocess(resolved) {
  const { serializeValue } = require('@bildit-platform/template-parser/v1')
  if (resolved == null) return 'null'
  if (typeof resolved === 'string') {
    return '{ text: ' + serializeValue(resolved) + ' }'
  }
  if (typeof resolved === 'object' && !Array.isArray(resolved)) {
    return serializeValue(resolved)
  }
  return 'null'
}

/**
 * Produces a JS object literal for Image so `field.url` / `field.alt` work after substitution.
 * Bare string defaults (`$(a:Image="https://…")`) are wrapped as `{ url, alt: "" }` so legacy
 * templates lifted by `array-config-extractor` keep working without grammar changes.
 * @param {unknown} resolved
 */
function serializeImageDefaultForPreprocess(resolved) {
  const { serializeValue } = require('@bildit-platform/template-parser/v1')
  if (resolved == null) return 'null'
  if (typeof resolved === 'string') {
    return '{ url: ' + serializeValue(resolved) + ', alt: "" }'
  }
  if (typeof resolved === 'object' && !Array.isArray(resolved)) {
    const url = typeof resolved.url === 'string' ? resolved.url : ''
    const alt = typeof resolved.alt === 'string' ? resolved.alt : ''
    return '{ url: ' + serializeValue(url) + ', alt: ' + serializeValue(alt) + ' }'
  }
  return 'null'
}

/**
 * Regex fallback when template-parser fails.
 * Replaces $(name:Type="default") with the default; $(name:Type) with null.
 * Handles BannerId/Component by replacing with null.
 */
function replaceTemplateVarsWithRegex(code) {
  return code.replace(TEMPLATE_VAR_REGEX, (_match, varName, type, _dropdownOptions, quotedDefault, unquotedDefault) => {
    const cleanType = (type || '').trim()
    if (cleanType === 'BannerId' || cleanType === 'Component') {
      return 'null'
    }
    const defaultValue = quotedDefault !== undefined ? quotedDefault : unquotedDefault
    const hasDefault = defaultValue !== undefined && defaultValue !== null && String(defaultValue).length > 0
    if (!hasDefault) {
      return 'null'
    }
    if (cleanType === 'Number') {
      const numVal = Number(defaultValue)
      return Number.isNaN(numVal) ? '0' : String(numVal)
    }
    if (cleanType === 'Boolean') {
      const strVal = String(defaultValue).toLowerCase()
      return strVal === 'true' ? 'true' : 'false'
    }
    if (cleanType === 'RichText') {
      const s = String(defaultValue).trim()
      // Object default: { text: "…", color: "…" }
      const m = s.match(/\btext\s*:\s*(?:"([^"]*)"|'([^']*)')/)
      const textValue = m ? (m[1] != null ? m[1] : m[2]) : s
      const te = String(textValue).replaceAll('\\', '\\\\').replaceAll('"', '\\"')
      const colM = s.match(
        /\bcolor\s*:\s*(?:"(#[0-9a-fA-F]{3,8})"|'(#[0-9a-fA-F]{3,8})'|"(#[0-9a-fA-F]+)"|'(#[0-9a-fA-F]+)'|"([^"]*)"|'([^']*)')/,
      )
      const col = colM
        ? colM[1] != null
          ? colM[1]
          : colM[2] != null
            ? colM[2]
            : colM[3] != null
              ? colM[3]
              : colM[4] != null
                ? colM[4]
                : colM[5] != null
                  ? colM[5]
                  : colM[6]
        : null
      if (col) {
        const ce = String(col).replaceAll('\\', '\\\\').replaceAll('"', '\\"')
        return `{ text: "${te}", color: "${ce}" }`
      }
      return `{ text: "${te}" }`
    }
    if (cleanType === 'Image') {
      const s = String(defaultValue).trim()
      // Object default: { url: "…", alt: "…" } — best-effort regex parse for the fallback path.
      const urlM = s.match(/\burl\s*:\s*(?:"([^"]*)"|'([^']*)')/)
      const altM = s.match(/\balt\s*:\s*(?:"([^"]*)"|'([^']*)')/)
      const urlValue = urlM ? (urlM[1] != null ? urlM[1] : urlM[2]) : s
      const altValue = altM ? (altM[1] != null ? altM[1] : altM[2]) : ''
      const ue = String(urlValue).replaceAll('\\', '\\\\').replaceAll('"', '\\"')
      const ae = String(altValue).replaceAll('\\', '\\\\').replaceAll('"', '\\"')
      return `{ url: "${ue}", alt: "${ae}" }`
    }
    const strValue = String(defaultValue)
    return `"${strValue.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`
  })
}

/**
 * True when the file is marked as a v2 template (same banner as codemod output).
 * V2 uses plain literals, not $(…); full-file template-parser would corrupt TS/TSX.
 * Scans the first lines so a `// {id}:v…` version banner may precede `// @template v2`.
 */
function hasV2TemplateBanner(code) {
  if (typeof code !== 'string') return false
  const lines = code.replace(/^\uFEFF/, '').split(/\r?\n/).slice(0, 30)
  return lines.some((line) => /^\s*\/\/\s*@template\s+v2\b/.test(line))
}

/**
 * The Lezer template grammar can match a **prefix** of `$(…)` when TypeScript appears inside
 * the parens (e.g. `$(id as String)`). Those spans have an empty `type` and an incomplete `raw`
 * (no closing `)`). Running transformTemplate then splices `null` into the line and produces
 * invalid code like `null(id as String) = …`. Only run the full-file transform when every
 * parsed expression looks like a real `$(name:Type…)` snippet.
 */
function templateParserExpressionsLookSafe(code) {
  try {
    const { parseTemplateExpressions } = require('@bildit-platform/template-parser/v1')
    const expressions = parseTemplateExpressions(code)
    for (const e of expressions) {
      const type = (e.type && String(e.type).trim()) || ''
      const raw = e.raw != null ? String(e.raw) : ''
      if (!type || !raw.endsWith(')')) return false
    }
    return true
  } catch {
    return false
  }
}

/**
 * Preprocesses code by replacing BILDIT template variables so it can be transpiled.
 * Uses template-parser (defaults or null); falls back to regex if parsing fails.
 *
 * @param {string} code - Raw code with $(varName:Type="default") syntax
 * @param {string} codeType - One of js, jsx, ts, tsx (html is skipped)
 * @returns {string} - Code with template vars replaced by defaults or null
 */
function preprocessBilditTemplate(code, codeType) {
  if (!code || typeof code !== 'string') return code
  if (codeType === 'html') return code

  code = canonicalizeDoubleParenAsCastTemplates(code, codeType)

  if (hasV2TemplateBanner(code)) return code
  if (!hasBilditTemplateVars(code)) return code

  try {
    const { transformTemplate } = require('@bildit-platform/template-parser/v1')
    if (!templateParserExpressionsLookSafe(code)) {
      return replaceTemplateVarsWithRegex(code)
    }
    // String RichText / Image defaults ("…") would otherwise become a string literal, breaking
    // `.text` / `.url` accessors emitted by the codemod.
    return transformTemplate(
      code,
      {},
      {
        RichText: serializeRichTextDefaultForPreprocess,
        Image: serializeImageDefaultForPreprocess,
      },
    )
  } catch {
    // Fallback to regex when template-parser fails (e.g. unsupported syntax)
    return replaceTemplateVarsWithRegex(code)
  }
}

/**
 * Returns true if code contains BILDIT template variable syntax.
 */
function hasBilditTemplateVars(code) {
  return typeof code === 'string' && /\$\([^)]+\)/.test(code)
}

const { parseTemplateMetadata } = require('@bildit-platform/template-parser/v1')

module.exports = {
  preprocessBilditTemplate,
  hasBilditTemplateVars,
  replaceTemplateVarsWithRegex,
  canonicalizeDoubleParenAsCastTemplates,
  /** Use this instead of ad-hoc `$(\)` regexes: supports `RichText` object and quoted string defaults. */
  parseTemplateMetadata,
  serializeRichTextDefaultForPreprocess,
  serializeImageDefaultForPreprocess,
}
