/**
 * Declarative YAML module loader.
 *
 * Reads .module.yaml files and generates Fastify CRUD routes at runtime.
 * No code generation — routes are created on the fly at startup.
 *
 * Usage:
 *   await loadDeclarativeModules(app, path.join(__dirname, '../modules/declarative'));
 */

import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { parse as parseYaml } from 'yaml';
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import crypto from 'node:crypto';
import {
  ModuleSchema,
  buildCreateSchema,
  buildUpdateSchema,
  defaultEndpoints,
  type ModuleDef,
  type EndpointDef,
} from './declarative-schema.js';

// ── Auth helpers ──────────────────────────────────────────────────────────

interface JwtPayload {
  sub: string;
  role?: string;
}

function getUserId(req: FastifyRequest): string {
  const jwt = (req as unknown as { jwtPayload?: JwtPayload }).jwtPayload;
  if (!jwt?.sub) throw Object.assign(new Error('Authentication required'), { statusCode: 401 });
  return jwt.sub;
}

function requireAdmin(req: FastifyRequest): string {
  const userId = getUserId(req);
  const jwt = (req as unknown as { jwtPayload?: JwtPayload }).jwtPayload;
  if (jwt?.role !== 'admin')
    throw Object.assign(new Error('Admin access required'), { statusCode: 403 });
  return userId;
}

function getProductId(req: FastifyRequest): string {
  const pid = req.headers['x-product-id'] as string | undefined;
  if (!pid) throw Object.assign(new Error('x-product-id header required'), { statusCode: 400 });
  return pid;
}

// ── In-memory store (for testing / no-DB mode) ───────────────────────────

export class MemoryStore {
  private collections = new Map<string, Map<string, Record<string, unknown>>>();

  getCollection(name: string): Map<string, Record<string, unknown>> {
    if (!this.collections.has(name)) {
      this.collections.set(name, new Map());
    }
    return this.collections.get(name)!;
  }

  async create(container: string, doc: Record<string, unknown>): Promise<Record<string, unknown>> {
    this.getCollection(container).set(doc.id as string, { ...doc });
    return doc;
  }

  async get(
    container: string,
    id: string,
    productId: string
  ): Promise<Record<string, unknown> | null> {
    const doc = this.getCollection(container).get(id);
    if (!doc || doc.productId !== productId) return null;
    return doc;
  }

  async list(
    container: string,
    productId: string,
    opts: {
      sortBy?: string;
      sortDir?: 'asc' | 'desc';
      limit?: number;
      offset?: number;
      filters?: Record<string, unknown>;
    } = {}
  ): Promise<{ items: Record<string, unknown>[]; total: number }> {
    let items = Array.from(this.getCollection(container).values()).filter(
      d => d.productId === productId
    );

    // Apply filters
    if (opts.filters) {
      for (const [key, value] of Object.entries(opts.filters)) {
        items = items.filter(d => d[key] === value);
      }
    }

    const total = items.length;

    // Sort
    if (opts.sortBy) {
      const dir = opts.sortDir === 'desc' ? -1 : 1;
      items.sort((a, b) => {
        const va = String(a[opts.sortBy!] ?? '');
        const vb = String(b[opts.sortBy!] ?? '');
        return va.localeCompare(vb) * dir;
      });
    }

    // Paginate
    const offset = opts.offset ?? 0;
    const limit = opts.limit ?? 50;
    items = items.slice(offset, offset + limit);

    return { items, total };
  }

  async update(
    container: string,
    id: string,
    productId: string,
    updates: Record<string, unknown>
  ): Promise<Record<string, unknown> | null> {
    const existing = await this.get(container, id, productId);
    if (!existing) return null;
    const updated = { ...existing, ...updates, updatedAt: new Date().toISOString() };
    this.getCollection(container).set(id, updated);
    return updated;
  }

  async delete(container: string, id: string, productId: string): Promise<boolean> {
    const existing = await this.get(container, id, productId);
    if (!existing) return false;
    this.getCollection(container).delete(id);
    return true;
  }
}

// ── Route generator ──────────────────────────────────────────────────────

/**
 * Register CRUD routes for a single declarative module definition.
 */
export function registerModuleRoutes(
  app: FastifyInstance,
  mod: ModuleDef,
  store: MemoryStore
): void {
  const createSchema = buildCreateSchema(mod);
  const updateSchema = buildUpdateSchema(mod);
  const endpoints =
    mod.endpoints && mod.endpoints.length > 0 ? mod.endpoints : defaultEndpoints(mod);

  for (const ep of endpoints) {
    if (ep.custom) continue; // Skip custom — those need hand-written handlers

    const handler = buildHandler(ep, mod, store, createSchema, updateSchema);
    const method = ep.method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete';
    app[method](ep.path, handler);
  }
}

function buildHandler(
  ep: EndpointDef,
  mod: ModuleDef,
  store: MemoryStore,
  createSchema: ReturnType<typeof buildCreateSchema>,
  updateSchema: ReturnType<typeof buildUpdateSchema>
): (req: FastifyRequest, reply: FastifyReply) => Promise<unknown> {
  const { method, path, auth } = ep;
  const isById = path.includes(':id');

  // Determine auth function
  const checkAuth = auth === 'admin' ? requireAdmin : auth === 'user' ? getUserId : null;

  // ── LIST ────────────────────────────────────────────────────────
  if (method === 'GET' && !isById) {
    return async (req, _reply) => {
      if (checkAuth) checkAuth(req);
      const productId = getProductId(req);
      const query = req.query as Record<string, string>;
      const sortBy = query.sortBy || 'createdAt';
      const sortDir = query.sortDir === 'asc' ? 'asc' : 'desc';
      const limit = Math.min(parseInt(query.limit || '50', 10), 100);
      const offset = parseInt(query.offset || '0', 10);

      // Extract field-based filters
      const filters: Record<string, unknown> = {};
      for (const key of Object.keys(mod.fields)) {
        if (query[key] !== undefined) {
          filters[key] = query[key];
        }
      }

      return store.list(mod.container, productId, { sortBy, sortDir, limit, offset, filters });
    };
  }

  // ── GET BY ID ───────────────────────────────────────────────────
  if (method === 'GET' && isById) {
    return async (req, _reply) => {
      if (checkAuth) checkAuth(req);
      const productId = getProductId(req);
      const { id } = req.params as { id: string };
      const doc = await store.get(mod.container, id, productId);
      if (!doc) throw Object.assign(new Error(`${mod.name} not found`), { statusCode: 404 });
      return doc;
    };
  }

  // ── CREATE ──────────────────────────────────────────────────────
  if (method === 'POST') {
    return async (req, reply) => {
      let userId: string | undefined;
      if (checkAuth) userId = checkAuth(req);
      const productId = getProductId(req);
      const input = createSchema.parse(req.body);
      const now = new Date().toISOString();
      const prefix = mod.idPrefix ? `${mod.idPrefix}_` : '';
      const doc: Record<string, unknown> = {
        id: `${prefix}${crypto.randomUUID()}`,
        productId,
        ...input,
        createdAt: now,
        updatedAt: now,
      };
      if (userId) doc.createdBy = userId;
      const created = await store.create(mod.container, doc);
      reply.status(201);
      return created;
    };
  }

  // ── UPDATE ──────────────────────────────────────────────────────
  if (method === 'PATCH' || method === 'PUT') {
    return async (req, _reply) => {
      if (checkAuth) checkAuth(req);
      const productId = getProductId(req);
      const { id } = req.params as { id: string };
      const updates = updateSchema.parse(req.body);
      const updated = await store.update(
        mod.container,
        id,
        productId,
        updates as Record<string, unknown>
      );
      if (!updated) throw Object.assign(new Error(`${mod.name} not found`), { statusCode: 404 });
      return updated;
    };
  }

  // ── DELETE ──────────────────────────────────────────────────────
  if (method === 'DELETE') {
    return async (req, reply) => {
      if (checkAuth) checkAuth(req);
      const productId = getProductId(req);
      const { id } = req.params as { id: string };
      const ok = await store.delete(mod.container, id, productId);
      if (!ok) throw Object.assign(new Error(`${mod.name} not found`), { statusCode: 404 });
      reply.status(204);
    };
  }

  // Fallback — shouldn't reach here
  return async () => ({ error: 'Unhandled endpoint' });
}

// ── YAML file loader ─────────────────────────────────────────────────────

/**
 * Parse a YAML string into a validated ModuleDef.
 */
export function parseModuleYaml(yamlContent: string): ModuleDef {
  const raw = parseYaml(yamlContent);
  return ModuleSchema.parse(raw);
}

/**
 * Load all .module.yaml files from a directory and register their routes.
 * Returns the list of loaded module definitions.
 */
export async function loadDeclarativeModules(
  app: FastifyInstance,
  dir: string,
  store?: MemoryStore
): Promise<ModuleDef[]> {
  const memStore = store ?? new MemoryStore();
  const modules: ModuleDef[] = [];

  let files: string[];
  try {
    files = await readdir(dir);
  } catch {
    // Directory doesn't exist — no declarative modules
    return modules;
  }

  const yamlFiles = files.filter(f => f.endsWith('.module.yaml'));

  for (const file of yamlFiles) {
    const content = await readFile(join(dir, file), 'utf-8');
    const mod = parseModuleYaml(content);
    registerModuleRoutes(app, mod, memStore);
    modules.push(mod);
    app.log.info(`Loaded declarative module: ${mod.name} (${file})`);
  }

  return modules;
}
