/**
 * Pure, reusable validation primitives. One function per concern so no
 * validator logic is duplicated across the schema, the config validator, or
 * the import boundary. All are side-effect free and framework-agnostic.
 */

const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
const HTTP_URL = /^https?:\/\/\S+$/;
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export interface StringCheckResult {
  readonly ok: boolean;
  /** Human-oriented detail for the failure (not an i18n key). */
  readonly error?: string;
}

export function isValidHexColor(value: string): boolean {
  return HEX_COLOR.test(value);
}

export function isValidHttpUrl(value: string): boolean {
  return HTTP_URL.test(value);
}

export function isValidEmail(value: string): boolean {
  return EMAIL.test(value);
}

/** Validates that a string parses as JSON. Empty/whitespace is treated as invalid. */
export function validateJson(raw: string): StringCheckResult {
  if (!raw || !raw.trim()) {
    return { ok: false, error: 'Empty JSON' };
  }
  try {
    JSON.parse(raw);
    return { ok: true };
  } catch (error) {
    return { ok: false, error: error instanceof Error ? error.message : 'Invalid JSON' };
  }
}

/**
 * Best-effort CSS syntax check: comments are stripped, then braces must be
 * balanced and every rule block must contain a selector. This is intentionally
 * lightweight (no full CSS parser); it catches the common breakages (unclosed
 * block, stray brace) without pulling in a parser dependency.
 */
export function validateCss(raw: string): StringCheckResult {
  const source = (raw ?? '').replace(/\/\*[\s\S]*?\*\//g, '');
  let depth = 0;
  for (const char of source) {
    if (char === '{') {
      depth++;
    } else if (char === '}') {
      depth--;
      if (depth < 0) {
        return { ok: false, error: 'Unexpected "}"' };
      }
    }
  }
  if (depth !== 0) {
    return { ok: false, error: 'Unbalanced braces' };
  }
  return { ok: true };
}

/** Extracts the CSS text of every `<style>` block in an HTML string. */
export function extractStyleBlocks(html: string): string[] {
  const blocks: string[] = [];
  const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
  let match: RegExpExecArray | null;
  while ((match = re.exec(html ?? '')) !== null) {
    blocks.push(match[1]);
  }
  return blocks;
}

/** Normalizes a route/slug for duplicate comparison: trim, strip leading/trailing slashes, lowercase. */
export function normalizeRoute(route: string): string {
  return (route ?? '').trim().replace(/^\/+|\/+$/g, '').toLowerCase();
}

export interface JsonSchemaLite {
  type?: 'object';
  /** Loosely typed to accept the wider `WidgetManifestEntry.settingsSchema` shape as-is - each entry's `type` is read defensively below. */
  properties?: Record<string, unknown>;
  required?: string[];
}

/**
 * Basic (type + required) JSON Schema check against an object - not a full
 * JSON Schema engine (no nested schemas, enums, formats, or $ref). Enough to
 * catch a widget's `props` missing a required field or holding the wrong
 * primitive type for a declared property, without pulling in a schema
 * validation dependency for a check this shallow.
 */
export function validateAgainstSchemaLite(value: Record<string, unknown>, schema: JsonSchemaLite): string[] {
  const errors: string[] = [];

  for (const key of schema.required ?? []) {
    if (value[key] === undefined || value[key] === null || value[key] === '') {
      errors.push(`missing required property "${key}"`);
    }
  }

  for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
    const propValue = value[key];
    const propType = propSchema && typeof propSchema === 'object' ? (propSchema as { type?: string }).type : undefined;
    if (propValue === undefined || !propType) {
      continue;
    }
    if (!matchesJsonSchemaType(propValue, propType)) {
      errors.push(`property "${key}" should be ${propType}`);
    }
  }

  return errors;
}

function matchesJsonSchemaType(value: unknown, type: string): boolean {
  switch (type) {
    case 'string': return typeof value === 'string';
    case 'number': case 'integer': return typeof value === 'number';
    case 'boolean': return typeof value === 'boolean';
    case 'array': return Array.isArray(value);
    case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value);
    default: return true;
  }
}

/** HTML void elements per the WHATWG spec: never require (or accept) a closing tag. */
const VOID_ELEMENTS = new Set([
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
  'link', 'meta', 'param', 'source', 'track', 'wbr',
]);

/**
 * Best-effort HTML well-formedness check: strips comments, then walks tags
 * with a stack, ignoring void/self-closing elements, and fails on a mismatched
 * or unclosed tag. Intentionally lightweight (no DOM parser dependency,
 * consistent with `validateCss` above) - it catches the common authoring
 * breakages (a tag left open, a stray closing tag) without validating full
 * HTML5 parsing semantics.
 */
export function validateHtml(raw: string): StringCheckResult {
  const source = (raw ?? '').replace(/<!--[\s\S]*?-->/g, '');
  const stack: string[] = [];
  const tagRe = /<\/?([a-zA-Z][a-zA-Z0-9-]*)[^>]*?(\/)?>/g;
  let match: RegExpExecArray | null;

  while ((match = tagRe.exec(source)) !== null) {
    const [full, rawName, selfClosingSlash] = match;
    const name = rawName.toLowerCase();
    const isClosing = full.startsWith('</');

    if (VOID_ELEMENTS.has(name) || selfClosingSlash) {
      continue;
    }

    if (isClosing) {
      // Strict top-of-stack match: a closing tag must close the innermost open
      // tag. Popping back to the nearest ancestor with the same name (instead
      // of requiring an exact match) would silently swallow a genuinely
      // unclosed inner tag - exactly the breakage this check exists to catch.
      if (stack.length === 0) {
        return { ok: false, error: `Unexpected closing tag </${name}>` };
      }
      if (stack[stack.length - 1] !== name) {
        return { ok: false, error: `Expected </${stack[stack.length - 1]}> but found </${name}>` };
      }
      stack.pop();
    } else {
      stack.push(name);
    }
  }

  if (stack.length > 0) {
    return { ok: false, error: `Unclosed tag <${stack[stack.length - 1]}>` };
  }
  return { ok: true };
}
