import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
import { pathToFileURL } from "node:url";
import { analyze, compile } from "./compile.js";
import { applyLayoutToModel } from "./layout.js";
import { validateModel } from "./validate.js";

const PORT = Number(process.env.PORT ?? "8787");
const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES ?? 2 * 1024 * 1024);
const CORS_ORIGIN = process.env.CORS_ORIGIN ?? "*";
const MAX_REQUESTS_PER_MINUTE = Number(process.env.MAX_REQUESTS_PER_MINUTE ?? 120);
const SCHEMETA_AUTH_TOKEN = String(process.env.SCHEMETA_AUTH_TOKEN ?? "").trim();
const FRONTEND_ROOT = join(process.cwd(), "frontend");
export const API_VERSION = "0.3.0";
export const SCHEMA_VERSION = "1.0.0";
const RATE_WINDOW_MS = 60_000;
const rateByClient = new Map();

const MIME_TYPES = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".svg": "image/svg+xml",
  ".png": "image/png"
};

function setCors(res) {
  res.setHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}

function json(res, status, payload) {
  setCors(res);
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(payload, null, 2));
}

export function errorEnvelope(code, message, details = {}) {
  const out = {
    ok: false,
    error: {
      code,
      message
    }
  };
  if (details && typeof details === "object") {
    Object.assign(out, details);
  }
  return out;
}

export function isAuthorizedRequest(req) {
  if (!SCHEMETA_AUTH_TOKEN) {
    return true;
  }
  const auth = req.headers?.authorization;
  if (typeof auth === "string") {
    const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
    if (m && m[1] === SCHEMETA_AUTH_TOKEN) {
      return true;
    }
  }
  const apiKey = req.headers?.["x-api-key"];
  if (typeof apiKey === "string" && apiKey.trim() === SCHEMETA_AUTH_TOKEN) {
    return true;
  }
  return false;
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    let size = 0;

    req.on("data", (chunk) => {
      const c = Buffer.from(chunk);
      size += c.length;
      if (size > MAX_BODY_BYTES) {
        reject(Object.assign(new Error("Payload too large"), { code: "PAYLOAD_TOO_LARGE" }));
        req.destroy();
        return;
      }
      chunks.push(c);
    });

    req.on("end", () => {
      if (chunks.length === 0) {
        resolve({});
        return;
      }

      try {
        const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
        resolve(value);
      } catch {
        reject(Object.assign(new Error("Malformed JSON"), { code: "INVALID_JSON" }));
      }
    });

    req.on("error", reject);
  });
}

function sanitizePath(urlPath) {
  if (urlPath === "/") {
    return "index.html";
  }

  const clean = normalize(urlPath).replace(/^\/+/, "");
  if (clean.includes("..")) {
    return null;
  }

  return clean;
}

async function serveStatic(urlPath, res) {
  const cleanPath = sanitizePath(urlPath);
  if (!cleanPath) {
    json(res, 400, errorEnvelope("invalid_path", "Invalid static file path."));
    return true;
  }

  const filePath = join(FRONTEND_ROOT, cleanPath);

  try {
    const content = await readFile(filePath);
    const type = MIME_TYPES[extname(filePath)] ?? "application/octet-stream";
    setCors(res);
    res.writeHead(200, { "Content-Type": type });
    res.end(content);
    return true;
  } catch {
    return false;
  }
}

export function parsePayloadOptions(body) {
  if (body && typeof body === "object" && Object.prototype.hasOwnProperty.call(body, "payload")) {
    return {
      payload: body.payload,
      options: body.options ?? {}
    };
  }

  return {
    payload: body,
    options: {}
  };
}

export function withEnvelopeMeta(payload) {
  const out = {
    api_version: API_VERSION,
    schema_version: SCHEMA_VERSION,
    ...payload
  };
  if (!out.request_id && payload?.request_id) {
    out.request_id = payload.request_id;
  }
  return out;
}

function requestIdFrom(req) {
  const headerId = req.headers?.["x-request-id"];
  if (typeof headerId === "string" && headerId.trim()) {
    return headerId.trim().slice(0, 128);
  }
  return randomUUID();
}

function auditLog(entry) {
  const line = {
    ts: new Date().toISOString(),
    service: "schemeta",
    ...entry
  };
  console.log(JSON.stringify(line));
}

function clientKey(req) {
  const forwarded = req.headers["x-forwarded-for"];
  if (typeof forwarded === "string" && forwarded.trim()) {
    return forwarded.split(",")[0].trim();
  }
  return req.socket?.remoteAddress ?? "unknown";
}

function pruneClientRate(now, list) {
  const minTs = now - RATE_WINDOW_MS;
  let idx = 0;
  while (idx < list.length && list[idx] < minTs) {
    idx += 1;
  }
  if (idx > 0) {
    list.splice(0, idx);
  }
}

function withinRateLimit(req) {
  if (!(MAX_REQUESTS_PER_MINUTE > 0)) {
    return true;
  }
  const key = clientKey(req);
  const now = Date.now();
  const list = rateByClient.get(key) ?? [];
  pruneClientRate(now, list);
  if (list.length >= MAX_REQUESTS_PER_MINUTE) {
    rateByClient.set(key, list);
    return false;
  }
  list.push(now);
  rateByClient.set(key, list);
  return true;
}

export function createRequestHandler() {
  return async (req, res) => {
  const requestId = requestIdFrom(req);
  const startedAt = Date.now();
  res.setHeader("x-request-id", requestId);
  res.on("finish", () => {
    auditLog({
      request_id: requestId,
      method: req.method,
      path: req.url,
      status: res.statusCode,
      duration_ms: Date.now() - startedAt,
      client: clientKey(req)
    });
  });

  if (!req.url || !req.method) {
    return json(res, 400, errorEnvelope("invalid_request", "Invalid request.", { request_id: requestId }));
  }

  const pathname = new URL(req.url, "http://localhost").pathname;

  if (req.method === "OPTIONS") {
    setCors(res);
    res.writeHead(204);
    res.end();
    return;
  }

  if (req.method === "GET" && pathname === "/health") {
    return json(res, 200, {
      ok: true,
      service: "schemeta",
      request_id: requestId,
      api_version: API_VERSION,
      schema_version: SCHEMA_VERSION,
      status: "ok",
      date: new Date().toISOString()
    });
  }

  if (req.method === "GET" && pathname === "/mcp/ui-bundle") {
    return json(res, 200, {
      ok: true,
      request_id: requestId,
      name: "schemeta-workspace",
      version: "0.2.0",
      api_version: API_VERSION,
      schema_version: SCHEMA_VERSION,
      entry: "/",
      title: "Schemeta Workspace",
      transport: "iframe"
    });
  }

  if (req.method === "POST" && pathname === "/analyze") {
    if (!isAuthorizedRequest(req)) {
      res.setHeader("WWW-Authenticate", "Bearer");
      return json(res, 401, errorEnvelope("unauthorized", "Missing or invalid API token.", { request_id: requestId }));
    }
    if (!withinRateLimit(req)) {
      return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly.", { request_id: requestId }));
    }
    try {
      const body = await readBody(req);
      const parsed = parsePayloadOptions(body);
      return json(res, 200, withEnvelopeMeta({ request_id: requestId, ...analyze(parsed.payload, parsed.options) }));
    } catch (err) {
      if (err?.code === "PAYLOAD_TOO_LARGE") {
        return json(res, 413, errorEnvelope("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, { request_id: requestId }));
      }
      if (err?.code === "INVALID_JSON") {
        return json(res, 400, errorEnvelope("invalid_json", "Invalid JSON payload.", { request_id: requestId }));
      }
      return json(res, 500, errorEnvelope("internal_error", "Request failed.", { request_id: requestId }));
    }
  }

  if (req.method === "POST" && pathname === "/compile") {
    if (!isAuthorizedRequest(req)) {
      res.setHeader("WWW-Authenticate", "Bearer");
      return json(res, 401, errorEnvelope("unauthorized", "Missing or invalid API token.", { request_id: requestId }));
    }
    if (!withinRateLimit(req)) {
      return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly.", { request_id: requestId }));
    }
    try {
      const body = await readBody(req);
      const parsed = parsePayloadOptions(body);
      return json(res, 200, withEnvelopeMeta({ request_id: requestId, ...compile(parsed.payload, parsed.options) }));
    } catch (err) {
      if (err?.code === "PAYLOAD_TOO_LARGE") {
        return json(res, 413, errorEnvelope("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, { request_id: requestId }));
      }
      if (err?.code === "INVALID_JSON") {
        return json(res, 400, errorEnvelope("invalid_json", "Invalid JSON payload.", { request_id: requestId }));
      }
      return json(res, 500, errorEnvelope("internal_error", "Request failed.", { request_id: requestId }));
    }
  }

  if (req.method === "POST" && pathname === "/layout/auto") {
    if (!isAuthorizedRequest(req)) {
      res.setHeader("WWW-Authenticate", "Bearer");
      return json(res, 401, errorEnvelope("unauthorized", "Missing or invalid API token.", { request_id: requestId }));
    }
    if (!withinRateLimit(req)) {
      return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly.", { request_id: requestId }));
    }
    try {
      const body = await readBody(req);
      const parsed = parsePayloadOptions(body);
      const validated = validateModel(parsed.payload, parsed.options);
      const model = validated.model ?? parsed.payload;
      const options = {
        ...(parsed.options ?? {}),
        use_elk_layout:
          Object.prototype.hasOwnProperty.call(parsed.options ?? {}, "use_elk_layout")
            ? parsed.options.use_elk_layout
            : true
      };
      const laidOut = applyLayoutToModel(model, { respectLocks: false, layoutOptions: options });
      return json(res, 200, {
        ok: true,
        request_id: requestId,
        api_version: API_VERSION,
        schema_version: SCHEMA_VERSION,
        model: laidOut,
        compile: withEnvelopeMeta(
          compile(laidOut, {
            ...options,
            preserve_placement: true
          })
        )
      });
    } catch (err) {
      if (err?.code === "PAYLOAD_TOO_LARGE") {
        return json(res, 413, errorEnvelope("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, { request_id: requestId }));
      }
      if (err?.code === "INVALID_JSON") {
        return json(res, 400, errorEnvelope("invalid_json", "Invalid JSON payload.", { request_id: requestId }));
      }
      return json(res, 500, errorEnvelope("internal_error", "Layout auto failed.", { request_id: requestId }));
    }
  }

  if (req.method === "POST" && pathname === "/layout/tidy") {
    if (!isAuthorizedRequest(req)) {
      res.setHeader("WWW-Authenticate", "Bearer");
      return json(res, 401, errorEnvelope("unauthorized", "Missing or invalid API token.", { request_id: requestId }));
    }
    if (!withinRateLimit(req)) {
      return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly.", { request_id: requestId }));
    }
    try {
      const body = await readBody(req);
      const parsed = parsePayloadOptions(body);
      const validated = validateModel(parsed.payload, parsed.options);
      const model = validated.model ?? parsed.payload;
      const options = {
        ...(parsed.options ?? {}),
        use_elk_layout:
          Object.prototype.hasOwnProperty.call(parsed.options ?? {}, "use_elk_layout")
            ? parsed.options.use_elk_layout
            : true
      };
      const laidOut = applyLayoutToModel(model, { respectLocks: true, layoutOptions: options });
      return json(res, 200, {
        ok: true,
        request_id: requestId,
        api_version: API_VERSION,
        schema_version: SCHEMA_VERSION,
        model: laidOut,
        compile: withEnvelopeMeta(
          compile(laidOut, {
            ...options,
            preserve_placement: true
          })
        )
      });
    } catch (err) {
      if (err?.code === "PAYLOAD_TOO_LARGE") {
        return json(res, 413, errorEnvelope("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, { request_id: requestId }));
      }
      if (err?.code === "INVALID_JSON") {
        return json(res, 400, errorEnvelope("invalid_json", "Invalid JSON payload.", { request_id: requestId }));
      }
      return json(res, 500, errorEnvelope("internal_error", "Layout tidy failed.", { request_id: requestId }));
    }
  }

  if (req.method === "GET") {
    const served = await serveStatic(pathname, res);
    if (served) {
      return;
    }
  }

  return json(res, 404, errorEnvelope("not_found", "Not found.", { request_id: requestId }));
  };
}

export const server = createServer(createRequestHandler());

export function startServer(port = PORT) {
  server.listen(port, () => {
    console.log(`Schemeta server listening on http://localhost:${port}`);
  });
}

const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
if (isMain) {
  startServer(PORT);
}
