import matter from 'gray-matter';
import { logger } from '../logger.js';

// ── Types ──────────────────────────────────────────────────────────────────────

export interface ParamSpec {
  name: string;
  type: 'string' | 'number' | 'boolean';
  description?: string;
  default?: string | number | boolean;
}

export interface ScriptMeta {
  description: string;
  params: ParamSpec[];
  sessionProfileId?: number;
  recordingSource?: string;
  createdAt?: string;
  updatedAt?: string;
}

export interface ParsedScript {
  frontmatter: ScriptMeta;
  body: string;
}

// ── Constants ─────────────────────────────────────────────────────────────────

const IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
const VALID_TYPES = new Set(['string', 'number', 'boolean']);
const KNOWN_KEYS = new Set([
  'description', 'params', 'session_profile_id',
  'recording_source', 'created_at', 'updated_at',
]);

// ── Parser ────────────────────────────────────────────────────────────────────

export function parseScript(source: string): ParsedScript {
  const parsed = matter(source);
  const data = parsed.data;
  if (data === null || typeof data !== 'object' || Array.isArray(data)) {
    throw new Error('invalid frontmatter: expected a YAML mapping');
  }
  const dataMap = data as Record<string, unknown>;
  // gray-matter preserves a leading "\n" in content when there's a blank line after the
  // closing "---". Strip it so body is canonical regardless of whether that blank line existed.
  const body = parsed.content.startsWith('\n') ? parsed.content.slice(1) : parsed.content;

  // No frontmatter block at all — data will be an empty object
  if (Object.keys(dataMap).length === 0) {
    return { frontmatter: { description: '', params: [] }, body: source };
  }

  // Warn on unknown keys
  for (const key of Object.keys(dataMap)) {
    if (!KNOWN_KEYS.has(key)) {
      logger.warn(`[frontmatter] unknown key "${key}" in script frontmatter`);
    }
  }

  // description
  const description = dataMap['description'] !== undefined ? String(dataMap['description']) : '';

  // params
  const rawParams = dataMap['params'];
  const params: ParamSpec[] = [];
  if (rawParams !== undefined) {
    if (!Array.isArray(rawParams)) {
      throw new Error('invalid frontmatter: params must be an array');
    }
    for (let i = 0; i < rawParams.length; i++) {
      const p = rawParams[i] as Record<string, unknown>;

      const name = p['name'];
      if (typeof name !== 'string' || !IDENTIFIER_RE.test(name)) {
        throw new Error(
          `invalid params[${i}].name: must be an identifier, got ${JSON.stringify(name)}`
        );
      }

      const type = p['type'];
      if (!VALID_TYPES.has(type as string)) {
        throw new Error(
          `invalid params[${i}].type: must be string | number | boolean, got ${JSON.stringify(type)}`
        );
      }

      const spec: ParamSpec = { name, type: type as ParamSpec['type'] };
      if (p['description'] !== undefined) spec.description = String(p['description']);
      if (p['default'] !== undefined) spec.default = p['default'] as string | number | boolean;
      params.push(spec);
    }
  }

  // session_profile_id
  let sessionProfileId: number | undefined;
  if (data['session_profile_id'] !== undefined) {
    const raw = data['session_profile_id'];
    if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
      throw new Error(
        `invalid session_profile_id: must be a positive integer, got ${JSON.stringify(raw)}`
      );
    }
    sessionProfileId = raw;
  }

  // recording_source
  const recordingSource =
    dataMap['recording_source'] !== undefined ? String(dataMap['recording_source']) : undefined;

  // created_at / updated_at
  const createdAt =
    dataMap['created_at'] !== undefined ? String(dataMap['created_at']) : undefined;
  const updatedAt =
    dataMap['updated_at'] !== undefined ? String(dataMap['updated_at']) : undefined;

  const frontmatter: ScriptMeta = { description, params };
  if (sessionProfileId !== undefined) frontmatter.sessionProfileId = sessionProfileId;
  if (recordingSource !== undefined) frontmatter.recordingSource = recordingSource;
  if (createdAt !== undefined) frontmatter.createdAt = createdAt;
  if (updatedAt !== undefined) frontmatter.updatedAt = updatedAt;

  return { frontmatter, body };
}

// ── Serializer ────────────────────────────────────────────────────────────────

export function serializeScript(script: ParsedScript): string {
  const { frontmatter, body } = script;
  const { description, params, sessionProfileId, recordingSource, createdAt, updatedAt } =
    frontmatter;

  // Build data in stable key order; omit undefined and empty params
  const data: Record<string, unknown> = { description };
  if (params && params.length > 0) data['params'] = params;
  if (sessionProfileId !== undefined) data['session_profile_id'] = sessionProfileId;
  if (recordingSource !== undefined) data['recording_source'] = recordingSource;
  if (createdAt !== undefined) data['created_at'] = createdAt;
  if (updatedAt !== undefined) data['updated_at'] = updatedAt;

  // matter.stringify prepends "---\n<yaml>\n---\n" to the body
  return matter.stringify('\n' + body, data);
}
