import crypto from "crypto";
import fs from "fs";
import os from "os";
import path from "path";
import { spawn } from "child_process";

const DEFAULT_POLL_TIMEOUT = 600_000;
const MAX_POLL_TIMEOUT = 600_000;
const MAX_EVENT_QUEUE = 100;
const MAX_SNIPPET = 2_000;
const STYLE_WRITE_EXTENSIONS = new Set([".css", ".scss", ".sass", ".less", ".html", ".jsx", ".tsx"]);

const APP_TARGETS = {
  studio_test: {
    appSlug: "studio_test",
    brandSlug: "studio_test",
    packageName: "@regen/studio",
    command: "pnpm --filter @regen/studio dev",
    url: "http://localhost:3011/editor/local-test-target",
  },
  "studio-test": {
    appSlug: "studio_test",
    brandSlug: "studio_test",
    packageName: "@regen/studio",
    command: "pnpm --filter @regen/studio dev",
    url: "http://localhost:3011/editor/local-test-target",
  },
  prt: {
    appSlug: "prt",
    brandSlug: "prt",
    packageName: "@regen/prt-portal",
    command: "pnpm --filter @regen/prt-portal dev",
    url: "http://localhost:3006",
  },
  "prt-portal": {
    appSlug: "prt",
    brandSlug: "prt",
    packageName: "@regen/prt-portal",
    command: "pnpm --filter @regen/prt-portal dev",
    url: "http://localhost:3006",
  },
};

function nowIso() {
  return new Date().toISOString();
}

function safeString(value, max = MAX_SNIPPET) {
  if (typeof value !== "string") return value;
  return value.length > max ? value.slice(0, max) : value;
}

function normalizeEvent(session, body) {
  const type = body.type || body.mode || "direct_edit";
  const id = body.id || `evt_${crypto.randomUUID()}`;
  const target = body.target && typeof body.target === "object" ? { ...body.target } : {};
  if (typeof target.textSnippet === "string") target.textSnippet = safeString(target.textSnippet, 500);
  if (typeof target.outerHTMLSnippet === "string") target.outerHTMLSnippet = safeString(target.outerHTMLSnippet, MAX_SNIPPET);
  if (typeof target.outerHTML === "string" && !target.outerHTMLSnippet) {
    target.outerHTMLSnippet = safeString(target.outerHTML, MAX_SNIPPET);
    delete target.outerHTML;
  }

  return {
    type,
    id,
    mode: body.mode || type,
    sessionId: session.sessionId,
    brandSlug: session.brandSlug,
    appSlug: session.appSlug,
    target,
    reference: body.reference || null,
    instruction: safeString(body.instruction || body.prompt || "", 4_000),
    createdAt: nowIso(),
  };
}

function readBody(req, maxBytes = 128 * 1024) {
  return new Promise((resolve, reject) => {
    let data = "";
    req.on("data", chunk => {
      data += chunk;
      if (data.length > maxBytes) reject(new Error("Body too large"));
    });
    req.on("end", () => {
      if (!data.trim()) return resolve({});
      try {
        resolve(JSON.parse(data));
      } catch {
        reject(new Error("Invalid JSON"));
      }
    });
    req.on("error", reject);
  });
}

function writeJson(res, status, data, cors) {
  res.writeHead(status, { "Content-Type": "application/json", ...cors });
  res.end(JSON.stringify(data));
}

function resolveSourceFile(session, file) {
  if (!file || typeof file !== "string") return null;
  const root = path.resolve(session.repoRoot || session.cwd || process.cwd());
  const candidate = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file);
  const rel = path.relative(root, candidate);
  if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
  if (!STYLE_WRITE_EXTENSIONS.has(path.extname(candidate).toLowerCase())) return null;
  return { root, path: candidate, relativePath: rel.replace(/\\/g, "/") };
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function applyCssTextEdit(source, property, value, lineNumber = null) {
  const propertyPattern = new RegExp(`(${escapeRegExp(property)}\\s*:\\s*)([^;\\n]+)(\\s*;?)`);
  if (Number.isInteger(lineNumber) && lineNumber > 0) {
    const lines = source.split(/\r?\n/);
    const index = Math.min(lines.length - 1, lineNumber - 1);
    const windowStart = Math.max(0, index - 8);
    const windowEnd = Math.min(lines.length - 1, index + 8);
    for (let i = windowStart; i <= windowEnd; i += 1) {
      if (propertyPattern.test(lines[i])) {
        lines[i] = lines[i].replace(propertyPattern, `$1${value}$3`);
        return { content: lines.join("\n"), strategy: "replace-near-line" };
      }
    }
    const anchor = lines[index] ?? "";
    const indent = anchor.match(/^\s*/)?.[0] ?? "";
    lines.splice(index + 1, 0, `${indent}${property}: ${value};`);
    return { content: lines.join("\n"), strategy: "insert-near-line" };
  }
  const replaced = source.replace(propertyPattern, `$1${value}$3`);
  if (replaced !== source) return { content: replaced, strategy: "replace-first" };
  throw new Error(`CSS property "${property}" was not found and no source line was provided.`);
}

function applyJsxStyleEdit(source, property, value, lineNumber = null) {
  const lines = source.split(/\r?\n/);
  const targetIndex = Number.isInteger(lineNumber) && lineNumber > 0 ? Math.min(lines.length - 1, lineNumber - 1) : -1;
  const windowStart = targetIndex >= 0 ? Math.max(0, targetIndex - 8) : 0;
  const windowEnd = targetIndex >= 0 ? Math.min(lines.length - 1, targetIndex + 8) : lines.length - 1;
  const camel = property.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
  const quoted = JSON.stringify(value);
  const propPattern = new RegExp(`(${camel}\\s*:\\s*)(["'\`])([^"'\`]+)(\\2)`);

  for (let i = windowStart; i <= windowEnd; i += 1) {
    if (propPattern.test(lines[i])) {
      lines[i] = lines[i].replace(propPattern, `$1${quoted}`);
      return { content: lines.join("\n"), strategy: "replace-js-style-near-line" };
    }
  }

  for (let i = windowStart; i <= windowEnd; i += 1) {
    if (lines[i].includes("style={{")) {
      lines[i] = lines[i].replace("style={{", `style={{ ${camel}: ${quoted},`);
      return { content: lines.join("\n"), strategy: "insert-js-style-near-line" };
    }
  }

  throw new Error(`No inline style object for "${property}" was found near the selected source line.`);
}

async function isUrlReachable(url) {
  try {
    const response = await fetch(url, {
      method: "GET",
      signal: AbortSignal.timeout(1_500),
    });
    return response.status < 500;
  } catch {
    return false;
  }
}

function resolveTarget(input = {}) {
  const slug = String(input.appSlug || input.brandSlug || "prt").toLowerCase();
  const configured = APP_TARGETS[slug];
  // No registration required — any appSlug proceeds. Sessions without a devUrl
  // or devCommand simply have no iframe target; the picker still works via extension.

  const base = configured || {
    appSlug: slug,
    brandSlug: input.brandSlug || slug,
    command: input.devCommand,
    url: input.devUrl,
  };

  return {
    appSlug: input.appSlug || base.appSlug,
    brandSlug: input.brandSlug || base.brandSlug || input.appSlug || base.appSlug,
    command: input.devCommand || base.command,
    url: input.devUrl || base.url,
    cwd: input.cwd || input.repoRoot || process.cwd(),
  };
}

function splitCommand(command) {
  if (!command || typeof command !== "string") return null;
  const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
  return parts.map(part => part.replace(/^"|"$/g, ""));
}

function startDevProcess(target, logFile) {
  const parts = splitCommand(target.command);
  if (!parts || parts.length === 0) {
    return { started: false, error: "missing_command" };
  }

  const logDir = path.join(os.tmpdir(), "clauth-studio-debug");
  fs.mkdirSync(logDir, { recursive: true });
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
  const outPath = path.join(logDir, `${target.appSlug}-${stamp}.out.log`);
  const errPath = path.join(logDir, `${target.appSlug}-${stamp}.err.log`);
  const out = fs.openSync(outPath, "a");
  const err = fs.openSync(errPath, "a");

  const proc = spawn(parts[0], parts.slice(1), {
    cwd: target.cwd,
    env: process.env,
    stdio: ["ignore", out, err],
    shell: process.platform === "win32",
    detached: true,
    windowsHide: true,
  });
  proc.unref();
  try {
    fs.appendFileSync(logFile, `[${nowIso()}] studio-debug dev start pid=${proc.pid} command=${target.command}\n`);
  } catch {}

  return {
    started: true,
    pid: proc.pid,
    command: target.command,
    url: target.url,
    stdout: outPath,
    stderr: errPath,
    process: proc,
  };
}

export class StudioDebugSessionStore {
  constructor({ port = 52437, logFile = path.join(os.tmpdir(), "clauth-serve.log"), dispatchAgent = null } = {}) {
    this.port = port;
    this.logFile = logFile;
    this.sessions = new Map();
    this.dispatchAgent = dispatchAgent;
  }

  async start(input = {}) {
    const target = resolveTarget(input);
    if (target.error) {
      return { ok: false, error: target.error, message: target.message, status: "error" };
    }

    const shouldLaunch = input.launchDevServer !== false;
    const reachable = await isUrlReachable(target.url);
    const launch = reachable
      ? { started: false, command: target.command, url: target.url, alreadyRunning: true }
      : shouldLaunch
        ? startDevProcess(target, this.logFile)
        : { started: false, command: target.command, url: target.url, skipped: true };

    const sessionId = input.sessionId || `studio-${crypto.randomUUID()}`;
    const token = crypto.randomBytes(24).toString("base64url");
    const session = {
      sessionId,
      token,
      brandSlug: target.brandSlug,
      appSlug: target.appSlug,
      repoRoot: input.repoRoot || target.cwd,
      cwd: target.cwd,
      devCommand: target.command,
      devUrl: target.url,
      modeDefault: input.modeDefault || "direct_edit",
      status: "waiting_for_agent",
      createdAt: nowIso(),
      updatedAt: nowIso(),
      stopped: false,
      events: [],
      replies: [],
      pendingPolls: [],
      launch,
    };
    this.sessions.set(sessionId, session);

    const relayBaseUrl = `http://127.0.0.1:${this.port}/studio/debug/${sessionId}`;
    const claudeBaseUrl = `http://127.0.0.1:${this.port}/studio/claude/${sessionId}`;
    const pollCommand = `node scripts/studio-debug-poll.mjs --session ${sessionId} --token ${token} --relay ${claudeBaseUrl}`;

    // When dispatchAgent is wired, events dispatch directly — no polling agent needed.
    if (this.dispatchAgent) {
      session.status = "waiting_for_event";
    }

    return {
      ok: true,
      sessionId,
      token,
      devUrl: target.url,
      relayBaseUrl,
      status: session.status,
      launch: {
        started: !!launch.started,
        alreadyRunning: !!launch.alreadyRunning,
        command: launch.command,
        url: launch.url,
        pid: launch.pid || null,
      },
      pollCommand,
      claudeBaseUrl,
    };
  }

  get(sessionId) {
    return this.sessions.get(sessionId) || null;
  }

  authenticate(sessionId, token) {
    const session = this.get(sessionId);
    if (!session) return { error: "not_found" };
    if (session.token !== token) return { error: "unauthorized" };
    return { session };
  }

  submitEvent(sessionId, body) {
    const auth = this.authenticate(sessionId, body.token);
    if (auth.error) return auth;
    const session = auth.session;
    const event = normalizeEvent(session, body);

    session.updatedAt = nowIso();
    session.status = "event_pending";

    // If a dispatchAgent handler exists, spawn a full headless Claude agent.
    if (this.dispatchAgent) {
      // Stash images from the event body so the dispatcher can write them to tmp
      if (Array.isArray(body.images) && body.images.length > 0) {
        session._pendingImages = body.images;
      }
      const prompt = [
        `You are a Studio local-debug agent. Make ONE source edit to the codebase.`,
        ``,
        `App: ${session.appSlug} (brand: ${session.brandSlug})`,
        `Repo: ${session.repoRoot || session.cwd}`,
        `Dev URL: ${session.devUrl || "unknown"}`,
        ``,
        `Edit event:`,
        `  Mode: ${event.mode || "direct_edit"}`,
        `  Selector: ${event.selector || "unknown"}`,
        `  Instruction: ${event.instruction || "Make the requested edit."}`,
        event.textSnippet ? `  Current text: "${event.textSnippet}"` : "",
        event.textEdit ? `  New text: "${event.textEdit.newText || ""}"` : "",
        event.file ? `  File hint: ${event.file}` : "",
        event.notes ? `  Notes: ${event.notes}` : "",
        ``,
        `Find the source file in the repo that renders this element and make the edit.`,
        `After editing, output a JSON object: {"status":"done","message":"<what you did>","filesChanged":["<path>"]}`,
      ].filter(Boolean).join("\n");

      this.dispatchAgent(event.id, session.token, null, session, prompt)
        .then(result => {
          let parsed = {};
          try { parsed = JSON.parse((result?.stdout || result?.output || "").trim()); } catch {}
          const reply = {
            eventId: event.id,
            status: parsed.status || "done",
            message: parsed.message || result?.stdout?.slice(0, 500) || "Agent completed",
            filesChanged: parsed.filesChanged || [],
            createdAt: nowIso(),
          };
          session.replies.push(reply);
          session.status = "done";
          session.updatedAt = nowIso();
        })
        .catch(() => {
          session.replies.push({
            eventId: event.id,
            status: "error",
            message: "Agent dispatch failed",
            filesChanged: [],
            createdAt: nowIso(),
          });
          session.status = "error";
          session.updatedAt = nowIso();
        });

      return { ok: true, eventId: event.id, status: session.status };
    }

    // Legacy path: queue for a polling agent
    if (session.pendingPolls.length > 0) {
      const poll = session.pendingPolls.shift();
      poll(event);
    } else {
      session.events.push(event);
      if (session.events.length > MAX_EVENT_QUEUE) session.events.shift();
    }
    return { ok: true, eventId: event.id, status: session.status };
  }

  poll(sessionId, token, timeoutMs = DEFAULT_POLL_TIMEOUT) {
    const auth = this.authenticate(sessionId, token);
    if (auth.error) return Promise.resolve(auth);
    const session = auth.session;
    if (session.events.length > 0) {
      session.status = "agent_received";
      session.updatedAt = nowIso();
      return Promise.resolve(session.events.shift());
    }
    if (session.stopped) return Promise.resolve({ type: "stopped" });

    const timeout = Math.min(Math.max(Number(timeoutMs) || DEFAULT_POLL_TIMEOUT, 1), MAX_POLL_TIMEOUT);
    session.status = "waiting_for_event";
    session.updatedAt = nowIso();

    return new Promise(resolve => {
      const timer = setTimeout(() => {
        session.pendingPolls = session.pendingPolls.filter(fn => fn !== finish);
        resolve({ type: "timeout" });
      }, timeout);
      const finish = event => {
        clearTimeout(timer);
        session.status = "agent_received";
        session.updatedAt = nowIso();
        resolve(event);
      };
      session.pendingPolls.push(finish);
    });
  }

  reply(sessionId, body) {
    const auth = this.authenticate(sessionId, body.token);
    if (auth.error) return auth;
    const session = auth.session;
    const reply = {
      eventId: body.eventId,
      status: body.status || body.type || "done",
      message: body.message || "",
      filesChanged: Array.isArray(body.filesChanged)
        ? body.filesChanged
        : body.file
          ? [body.file]
          : [],
      createdAt: nowIso(),
    };
    session.replies.push(reply);
    session.status = reply.status === "done" ? "done" : reply.status;
    session.updatedAt = nowIso();
    return { ok: true, status: session.status, reply };
  }

  writeStyle(sessionId, body) {
    const auth = this.authenticate(sessionId, body.token);
    if (auth.error) return auth;
    const session = auth.session;
    const styleEdit = body.styleEdit && typeof body.styleEdit === "object" ? body.styleEdit : {};
    const deltas = Array.isArray(styleEdit.deltas) ? styleEdit.deltas : [];
    const source = styleEdit.source && typeof styleEdit.source === "object" ? styleEdit.source : {};
    const fileInfo = resolveSourceFile(session, source.file || body.file);
    if (!fileInfo) return { error: "invalid_source", message: "style-write requires a source file inside the debug session repo." };
    if (deltas.length !== 1) return { error: "invalid_delta", message: "style-write currently accepts exactly one deterministic CSS delta." };

    const delta = deltas[0] || {};
    if (typeof delta.property !== "string" || typeof delta.value !== "string") {
      return { error: "invalid_delta", message: "style-write requires string property and value." };
    }

    try {
      const original = fs.readFileSync(fileInfo.path, "utf8");
      const ext = path.extname(fileInfo.path).toLowerCase();
      const result = ext === ".jsx" || ext === ".tsx"
        ? applyJsxStyleEdit(original, delta.property, delta.value, source.line || body.line || null)
        : applyCssTextEdit(original, delta.property, delta.value, source.line || body.line || null);
      fs.writeFileSync(fileInfo.path, result.content, "utf8");
      const reply = {
        eventId: body.id || `style_${crypto.randomUUID()}`,
        status: "done",
        message: `Direct style write (${result.strategy})`,
        filesChanged: [fileInfo.relativePath],
        createdAt: nowIso(),
      };
      session.replies.push(reply);
      session.status = "done";
      session.updatedAt = nowIso();
      return { ok: true, status: "done", eventId: reply.eventId, filesChanged: reply.filesChanged, strategy: result.strategy };
    } catch (err) {
      return { error: "style_write_failed", message: err instanceof Error ? err.message : String(err) };
    }
  }

  status(sessionId, token) {
    const auth = this.authenticate(sessionId, token);
    if (auth.error) return auth;
    const session = auth.session;
    return {
      ok: true,
      sessionId: session.sessionId,
      brandSlug: session.brandSlug,
      appSlug: session.appSlug,
      devUrl: session.devUrl,
      status: session.status,
      createdAt: session.createdAt,
      updatedAt: session.updatedAt,
      pendingEvents: session.events.length,
      pendingPolls: session.pendingPolls.length,
      replies: session.replies,
      launch: {
        started: !!session.launch?.started,
        alreadyRunning: !!session.launch?.alreadyRunning,
        command: session.launch?.command || session.devCommand,
        url: session.launch?.url || session.devUrl,
        pid: session.launch?.pid || null,
      },
    };
  }

  stop(sessionId, token) {
    const auth = this.authenticate(sessionId, token);
    if (auth.error) return auth;
    const session = auth.session;
    session.stopped = true;
    session.status = "stopped";
    session.updatedAt = nowIso();
    if (session.launch?.process && !session.launch.process.killed) {
      try {
        session.launch.process.kill();
      } catch {}
    }
    for (const poll of session.pendingPolls.splice(0)) poll({ type: "stopped" });
    this.sessions.delete(sessionId);
    return { ok: true, status: "stopped", sessionId };
  }
}

export function createStudioDebugRuntime(options) {
  const store = new StudioDebugSessionStore(options);

  async function handle(req, res, url, cors) {
    const reqPath = url.pathname;
    const method = req.method;

    if (method === "POST" && reqPath === "/studio/debug/start") {
      try {
        const body = await readBody(req);
        const result = await store.start(body);
        return writeJson(res, result.ok ? 200 : 400, result, cors);
      } catch (err) {
        return writeJson(res, 400, { ok: false, error: err.message }, cors);
      }
    }

    const debugMatch = reqPath.match(/^\/studio\/debug\/([^/]+)\/(events|style-write|status|stop)$/);
    const claudeMatch = reqPath.match(/^\/studio\/claude\/([^/]+)\/(poll|reply|status)$/);
    if (!debugMatch && !claudeMatch) return false;
    const [, sessionId, action] = debugMatch || claudeMatch;

    try {
      if (method === "POST" && action === "events") {
        const result = store.submitEvent(sessionId, await readBody(req));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
      }
      if (method === "POST" && action === "style-write") {
        const result = store.writeStyle(sessionId, await readBody(req));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 400 : 200, result, cors);
      }
      if (method === "GET" && action === "poll") {
        const result = await store.poll(sessionId, url.searchParams.get("token"), url.searchParams.get("timeout"));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
      }
      if (method === "POST" && action === "reply") {
        const result = store.reply(sessionId, await readBody(req));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
      }
      if (method === "GET" && action === "status") {
        const result = store.status(sessionId, url.searchParams.get("token"));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
      }
      if (method === "POST" && action === "stop") {
        const body = await readBody(req);
        const result = store.stop(sessionId, body.token || url.searchParams.get("token"));
        return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
      }
      return writeJson(res, 405, { ok: false, error: "method_not_allowed" }, cors);
    } catch (err) {
      return writeJson(res, 400, { ok: false, error: err.message }, cors);
    }
  }

  return { store, handle };
}

export const studioDebugTargets = APP_TARGETS;
