// Pure plugin-package validation at the installation boundary: schema and size limits, GLSL tokens,
// CUBE dry runs, and envelope ranges. Browser-side compilation stays in install.ts; npm test runs this file through tsx.
import { parseCube } from "../gl/fx/cube.js";
import {
  ITEM_ID_RE,
  PACK_ID_RE,
  PLUGIN_FORMAT,
  PLUGIN_LIMITS,
  PROP_KEY_RE,
  type PluginNumberProp,
  type PluginPack,
} from "./types.js";

export type ValidateResult =
  { ok: true; pack: PluginPack } | { ok: false; errors: string[] };

const isObj = (v: unknown): v is Record<string, unknown> =>
  !!v && typeof v === "object" && !Array.isArray(v);
const isStr = (v: unknown): v is string => typeof v === "string";
const isNum = (v: unknown): v is number =>
  typeof v === "number" && Number.isFinite(v);
const bytes = (s: string): number => new TextEncoder().encode(s).length;

function checkName(errors: string[], at: string, v: unknown): void {
  if (!isStr(v) || !v.trim() || v.length > PLUGIN_LIMITS.maxNameLen) {
    errors.push(
      `${at}: name 必须是 1..${PLUGIN_LIMITS.maxNameLen} 字符的字符串`,
    );
  }
}

function checkProps(
  errors: string[],
  at: string,
  v: unknown,
): PluginNumberProp[] | undefined {
  if (v === undefined) return undefined;
  if (!Array.isArray(v) || v.length > PLUGIN_LIMITS.maxProps) {
    errors.push(`${at}: props 必须是 ≤${PLUGIN_LIMITS.maxProps} 项的数组`);
    return undefined;
  }
  const seen = new Set<string>();
  for (const [i, p] of v.entries()) {
    const where = `${at}.props[${i}]`;
    if (!isObj(p)) {
      errors.push(`${where}: 必须是对象`);
      continue;
    }
    if (!isStr(p.key) || !PROP_KEY_RE.test(p.key))
      errors.push(`${where}: key 非法(${PROP_KEY_RE})`);
    else if (seen.has(p.key)) errors.push(`${where}: key 重复 ${p.key}`);
    else seen.add(p.key);
    if (!isStr(p.label) || !p.label.trim()) errors.push(`${where}: label 缺失`);
    if (!isNum(p.default) || !isNum(p.min) || !isNum(p.max) || p.min > p.max) {
      errors.push(`${where}: default/min/max 必须是有限数且 min≤max`);
    }
    if (p.step !== undefined && (!isNum(p.step) || p.step <= 0))
      errors.push(`${where}: step 必须 >0`);
  }
  return v as PluginNumberProp[];
}

function checkFrag(
  errors: string[],
  at: string,
  frag: unknown,
  requiredTokens: string[],
): void {
  if (!isStr(frag) || !frag.trim()) {
    errors.push(`${at}: frag 缺失`);
    return;
  }
  if (bytes(frag) > PLUGIN_LIMITS.maxFragBytes)
    errors.push(`${at}: frag 超过 ${PLUGIN_LIMITS.maxFragBytes / 1024}KB`);
  for (const token of requiredTokens) {
    if (!frag.includes(token)) errors.push(`${at}: frag 必须引用 ${token}`);
  }
}

// data:image/* inline (limited length) or origin path/https; other schemes (javascript:, etc.) are rejected
function checkThumb(errors: string[], at: string, v: unknown): void {
  if (v === undefined) return;
  if (!isStr(v) || !v.trim()) {
    errors.push(`${at}: thumb 必须是非空字符串`);
    return;
  }
  if (v.startsWith("data:image/")) {
    if (bytes(v) > PLUGIN_LIMITS.maxThumbBytes)
      errors.push(`${at}: thumb 超过 ${PLUGIN_LIMITS.maxThumbBytes / 1024}KB`);
    return;
  }
  if (
    !v.startsWith("/") &&
    !v.startsWith("https://") &&
    !v.startsWith("http://")
  ) {
    errors.push(`${at}: thumb 只允许 data:image/* 或 URL(/… | https://…)`);
  }
}

function checkItem(errors: string[], item: unknown, index: number): void {
  const at = `items[${index}]`;
  if (!isObj(item)) {
    errors.push(`${at}: 必须是对象`);
    return;
  }
  if (!isStr(item.id) || !ITEM_ID_RE.test(item.id))
    errors.push(`${at}: id 非法(${ITEM_ID_RE})`);
  checkName(errors, at, item.name);
  if (
    item.desc !== undefined &&
    (!isStr(item.desc) || item.desc.length > PLUGIN_LIMITS.maxDescLen)
  ) {
    errors.push(`${at}: desc 超长(≤${PLUGIN_LIMITS.maxDescLen})`);
  }
  checkThumb(errors, at, item.thumb);
  switch (item.type) {
    case "mg-template": {
      if (!isStr(item.code) || !item.code.trim())
        errors.push(`${at}: code 缺失`);
      else if (bytes(item.code) > PLUGIN_LIMITS.maxCodeBytes)
        errors.push(`${at}: code 超过 ${PLUGIN_LIMITS.maxCodeBytes / 1024}KB`);
      for (const dim of ["width", "height"] as const) {
        const v = item[dim];
        if (v !== undefined && (!isNum(v) || v < 16 || v > 8192))
          errors.push(`${at}: ${dim} 必须在 [16, 8192]`);
      }
      const duration = item.durationInFrames;
      if (duration !== undefined && (!isNum(duration) || duration < 1 || duration > 216_000))
        errors.push(`${at}: durationInFrames 必须在 [1, 216000]`);
      if (item.props !== undefined && !isObj(item.props))
        errors.push(`${at}: props 必须是对象`);
      if (item.propSchema !== undefined) {
        if (!Array.isArray(item.propSchema) || item.propSchema.length > 32) {
          errors.push(`${at}: propSchema 必须是 ≤32 项的数组`);
        } else {
          for (const [i, s] of item.propSchema.entries()) {
            if (!isObj(s) || !isStr(s.key) || !isStr(s.type)) {
              errors.push(`${at}.propSchema[${i}]: 需要 key/type 字符串`);
            }
          }
        }
      }
      return;
    }
    case "transition": {
      checkFrag(errors, at, item.frag, [
        "u_outgoing",
        "u_incoming",
        "u_progress",
      ]);
      checkProps(errors, at, item.props);
      const d = item.defaultDurationFrames;
      if (d !== undefined && (!isNum(d) || d < 2 || d > 300))
        errors.push(`${at}: defaultDurationFrames 必须在 [2, 300]`);
      return;
    }
    case "fx": {
      checkFrag(errors, at, item.frag, ["u_input"]);
      checkProps(errors, at, item.props);
      if (item.passes !== undefined) {
        if (
          !Array.isArray(item.passes) ||
          item.passes.length < 1 ||
          item.passes.length > 4
        ) {
          errors.push(`${at}: passes 必须是 1..4 段的数组`);
        } else {
          for (const [i, pass] of item.passes.entries())
            checkFrag(errors, `${at}.passes[${i}]`, pass, []);
        }
      }
      return;
    }
    case "lut": {
      const hasCube = isStr(item.cube) && !!item.cube.trim();
      const hasFrag = isStr(item.frag) && !!item.frag.trim();
      if (hasCube === hasFrag) {
        errors.push(`${at}: cube 与 frag 必须且只能提供一个`);
        return;
      }
      if (hasFrag) {
        checkFrag(errors, at, item.frag, ["u_input"]);
        checkProps(errors, at, item.props);
        return;
      }
      if (bytes(item.cube as string) > PLUGIN_LIMITS.maxCubeBytes) {
        errors.push(
          `${at}: cube 超过 ${PLUGIN_LIMITS.maxCubeBytes / 1024 / 1024}MB`,
        );
        return;
      }
      try {
        parseCube(item.cube as string);
      } catch (e) {
        errors.push(
          `${at}: cube 解析失败 — ${e instanceof Error ? e.message : String(e)}`,
        );
      }
      return;
    }
    case "zoom": {
      const env = item.envelope;
      const shapes = new Set([
        "hold", "punch", "slow-push", "instant", "zoom-out", "ease-in",
        "bounce", "snap", "pulse", "whip-in",
      ]);
      if (env === undefined && item.shape === undefined) {
        errors.push(`${at}: envelope 与 shape 至少提供一个`);
      } else if (env !== undefined && (
        !Array.isArray(env)
        || env.length < PLUGIN_LIMITS.minEnvelopePoints
        || env.length > PLUGIN_LIMITS.maxEnvelopePoints
      )) {
        errors.push(
          `${at}: envelope 必须是 ${PLUGIN_LIMITS.minEnvelopePoints}..${PLUGIN_LIMITS.maxEnvelopePoints} 个点`,
        );
      } else if (Array.isArray(env) &&
        !env.every(
          (v) => isNum(v) && v >= 0 && v <= PLUGIN_LIMITS.maxEnvelopeValue,
        )
      ) {
        errors.push(
          `${at}: envelope 值必须在 [0, ${PLUGIN_LIMITS.maxEnvelopeValue}]`,
        );
      }
      if (item.shape !== undefined && (!isStr(item.shape) || !shapes.has(item.shape)))
        errors.push(`${at}: shape 非法`);
      const mag = item.magnification;
      if (mag !== undefined && (!isNum(mag) || mag < 1 || mag > 16))
        errors.push(`${at}: magnification 必须在 [1, 16]`);
      for (const key of ["focalPointX", "focalPointY"] as const) {
        const value = item[key];
        if (value !== undefined && (!isNum(value) || value < 0 || value > 1))
          errors.push(`${at}: ${key} 必须在 [0, 1]`);
      }
      for (const key of ["easeInFrames", "easeOutFrames"] as const) {
        const value = item[key];
        if (value !== undefined && (!isNum(value) || value < 0 || value > 300))
          errors.push(`${at}: ${key} 必须在 [0, 300]`);
      }
      return;
    }
    default:
      errors.push(`${at}: 未知 type ${String(item.type)}`);
  }
}

/** Verify a plugin package JSON (untrusted input). Returns ok only after all pass. */
export function validatePack(v: unknown): ValidateResult {
  const errors: string[] = [];
  if (!isObj(v)) return { ok: false, errors: ["插件包必须是 JSON 对象"] };
  if (v.format !== PLUGIN_FORMAT) {
    errors.push(
      `format 必须是 "${PLUGIN_FORMAT}"(当前仅支持该版本;未知 format 拒装)`,
    );
  }
  if (!isStr(v.id) || !PACK_ID_RE.test(v.id))
    errors.push(`包 id 非法(${PACK_ID_RE})`);
  checkName(errors, "pack", v.name);
  if (!isStr(v.version) || !/^\d+\.\d+\.\d+$/.test(v.version))
    errors.push("version 必须是 x.y.z");
  if (
    v.author !== undefined &&
    (!isStr(v.author) || v.author.length > PLUGIN_LIMITS.maxNameLen)
  )
    errors.push("author 超长");
  if (
    v.description !== undefined &&
    (!isStr(v.description) || v.description.length > PLUGIN_LIMITS.maxDescLen)
  )
    errors.push("description 超长");
  if (
    !Array.isArray(v.items) ||
    v.items.length < 1 ||
    v.items.length > PLUGIN_LIMITS.maxItems
  ) {
    errors.push(`items 必须是 1..${PLUGIN_LIMITS.maxItems} 条`);
  } else {
    const ids = new Set<string>();
    for (const [i, item] of v.items.entries()) {
      checkItem(errors, item, i);
      const id = isObj(item) && isStr(item.id) ? item.id : null;
      if (id) {
        if (ids.has(id)) errors.push(`items[${i}]: id 重复 ${id}`);
        ids.add(id);
      }
    }
  }
  if (errors.length) return { ok: false, errors };
  return {
    ok: true,
    pack: { ...v, format: PLUGIN_FORMAT } as unknown as PluginPack,
  };
}

/** Verification of a single piece of content (for use by "Export as plugin"/editor built-in stream)*/
export function validateItem(item: unknown): string[] {
  const errors: string[] = [];
  checkItem(errors, item, 0);
  return errors;
}