// systemActions.ts
// Scope: system/root file-management actions kept separate from Recorder content-edit actions.

import fs from "node:fs/promises";
import path from "node:path";
import { logCode, logSystem } from "./playbackCore";
import { getActiveProjectRecord, getSettings } from "./mgrSNP";
import { getUserTier } from "./hypauth/auth";

export type SystemActionName =
  | "clearfile"
  | "delete"
  | "copy"
  | "move"
  | "rename";

export type SystemActionManifest = {
  Task: string;
  Action: SystemActionName | string;
  FilePath: string;
  Payload?: unknown;
  Comment?: string | null;
  [key: string]: any;
};

export type EditorSystemActionInput = {
  action: SystemActionName | string;
  filePath: string;
  payload?: unknown;
};

export type SystemActionResult = {
  ok: boolean;
  action: string;
  filePath: string;
  changed: boolean;
  message: string;
  backupPath?: string;
  destinationPath?: string;
};

export const SYSTEM_ACTIONS = new Set<SystemActionName>([
  "clearfile",
  "delete",
  "copy",
  "move",
  "rename"
]);

const ROOT = process.cwd();

export function normalizeSystemAction(value: unknown): string {
  return String(value || "").trim().toLowerCase();
}

export function isSystemAction(value: unknown): value is SystemActionName {
  return SYSTEM_ACTIONS.has(normalizeSystemAction(value) as SystemActionName);
}

function isWithinRoot(absPath: string, rootPath: string): boolean {
  const abs = path.resolve(absPath);
  const root = path.resolve(rootPath);
  return abs === root || abs.startsWith(root + path.sep);
}

function resolveProjectPath(projectPath: string): string {
  const clean = String(projectPath || "").trim();
  if (!clean) return path.resolve(ROOT, "projects");
  return path.isAbsolute(clean) ? path.resolve(clean) : path.resolve(ROOT, clean);
}

function isBackstage(): boolean {
  return String(getUserTier() || "").trim().toLowerCase() === "backstage";
}

function isUniversalProject(project: any): boolean {
  return String(project?.kind || "").toLowerCase() === "universal"
    || String(project?.name || "").trim().toLowerCase() === "universal";
}

function getActiveProjectRoot(): string {
  const activeProject = getActiveProjectRecord();
  return resolveProjectPath(activeProject?.location || activeProject?.path || "./projects");
}

function getDefaultResolveRoot(): string {
  const activeProject = getActiveProjectRecord();
  if (isBackstage() && isUniversalProject(activeProject)) return path.resolve(ROOT);
  return getActiveProjectRoot();
}

function isExplicitAbsolutePath(filePath: string): boolean {
  const clean = String(filePath || "").trim();
  return /^[A-Za-z]:[\\/]/.test(clean) || clean.startsWith("\\\\");
}

function getPermittedRoots(): string[] {
  const settings = getSettings();
  const roots = new Set<string>();

  for (const project of Array.isArray(settings.projects) ? settings.projects : []) {
    const projectPath = String(project?.location || project?.path || "").trim();
    if (projectPath) roots.add(resolveProjectPath(projectPath));
  }

  roots.add(getActiveProjectRoot());
  if (isBackstage()) roots.add(path.resolve(ROOT));
  return [...roots];
}
function getAllowedWriteRoots(): string[] {
  const settings = getSettings();
  const activeProject = getActiveProjectRecord();
  const activeRoot = getActiveProjectRoot();

  if (settings.restrictEditsToProject && activeProject && !isUniversalProject(activeProject)) return [activeRoot];
  return getPermittedRoots();
}

function resolveFile(filePath: string): string {
  const clean = String(filePath || "").trim();
  if (!clean) throw new Error("systemaction_filepath_missing");

  const defaultRoot = getDefaultResolveRoot();
  const normalized = clean.replace(/\\/g, "/").replace(/^\.\//, "");
  const projectRelative = normalized.replace(/^\/+/, "");
  const rootRelative = projectRelative === "projects" || projectRelative.startsWith("projects/") || projectRelative === "logs" || projectRelative.startsWith("logs/");
  const abs = isExplicitAbsolutePath(clean) ? path.resolve(clean) : path.resolve(rootRelative ? ROOT : defaultRoot, projectRelative);
  const allowedRoots = getAllowedWriteRoots();

  if (allowedRoots.some(root => isWithinRoot(abs, root))) return abs;

  const settings = getSettings();
  throw new Error(settings.restrictEditsToProject ? "project_scope_blocked" : "path_outside_permitted_roots");
}

function payloadObject(payload: unknown, action: string): Record<string, any> {
  if (payload && typeof payload === "object" && !Array.isArray(payload)) return payload as Record<string, any>;

  if (typeof payload === "string") {
    const clean = payload.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
    if (clean.startsWith("{") && clean.endsWith("}")) {
      try {
        const parsed = JSON.parse(clean);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, any>;
      } catch {}
    }
  }

  throw new Error(`${action}_payload_must_be_object`);
}

function destinationFromPayload(payload: unknown, action: string): string {
  const body = payloadObject(payload, action);
  const destination = body.to ?? body.destination ?? body.target ?? body.FilePath ?? body.filePath;
  if (typeof destination !== "string" || !destination.trim()) throw new Error(`${action}_destination_missing`);
  return destination.trim();
}

function renameTargetFromPayload(payload: unknown): string {
  const body = payloadObject(payload, "rename");
  const name = body.name ?? body.newName ?? body.to ?? body.destination ?? body.target;
  if (typeof name !== "string" || !name.trim()) throw new Error("rename_name_missing");
  const clean = name.trim();
  if (clean.includes("/") || clean.includes("\\")) throw new Error("rename_name_must_be_basename");
  return clean;
}

function clearContentFromPayload(payload: unknown): string {
  if (payload == null) return "";
  if (typeof payload === "string") return payload;
  const body = payloadObject(payload, "clearfile");
  const content = body.content ?? body.template ?? body.reset ?? "";
  if (typeof content !== "string") throw new Error("clearfile_content_must_be_string");
  return content;
}

async function readText(filePath: string): Promise<string> {
  return await fs.readFile(filePath, "utf8");
}

async function writeText(filePath: string, content: string): Promise<void> {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
  await fs.writeFile(filePath, content, "utf8");
}

async function ensureMissing(filePath: string, action: string): Promise<void> {
  try {
    await fs.access(filePath);
    throw new Error(`${action}_target_already_exists`);
  } catch (err: any) {
    if (err?.message === `${action}_target_already_exists`) throw err;
  }
}

function deletedBackupPath(filePath: string): string {
  const relative = path.relative(ROOT, filePath).replace(/\\/g, "/");
  const safeRelative = relative.replace(/^\.\.\//g, "outside/").replace(/[:*?"<>|]/g, "_");
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
  return path.join(ROOT, "logs", "deleted", `${stamp}__${safeRelative}`);
}

async function applyClearFile(filePath: string, payload: unknown): Promise<SystemActionResult> {
  const before = await readText(filePath);
  const after = clearContentFromPayload(payload);
  await writeText(filePath, after);
  if (await readText(filePath) !== after) throw new Error("clearfile_verification_failed");
  logCode({ action: "clearfile", filePath, before, after, changed: before !== after, outcome: "cleared" });
  return { ok: true, action: "clearfile", filePath, changed: before !== after, message: "file cleared" };
}

async function applyDelete(filePath: string): Promise<SystemActionResult> {
  const before = await readText(filePath);
  const backupPath = deletedBackupPath(filePath);
  await writeText(backupPath, before);
  if (await readText(backupPath) !== before) throw new Error("delete_backup_verification_failed");
  await fs.unlink(filePath);
  logSystem("Editor", { event: "editor_file_deleted", filePath, backupPath, changed: true });
  return { ok: true, action: "delete", filePath, changed: true, message: "file deleted with backup", backupPath };
}

async function applyCopy(filePath: string, payload: unknown): Promise<SystemActionResult> {
  const destination = resolveFile(destinationFromPayload(payload, "copy"));
  await ensureMissing(destination, "copy");
  const before = await readText(filePath);
  await writeText(destination, before);
  if (await readText(destination) !== before) throw new Error("copy_verification_failed");
  logSystem("Editor", { event: "editor_file_copied", filePath, destinationPath: destination, changed: true });
  return { ok: true, action: "copy", filePath, changed: true, message: "file copied", destinationPath: destination };
}

async function applyMove(filePath: string, payload: unknown): Promise<SystemActionResult> {
  const destination = resolveFile(destinationFromPayload(payload, "move"));
  await ensureMissing(destination, "move");
  const before = await readText(filePath);
  await fs.mkdir(path.dirname(destination), { recursive: true });
  await fs.rename(filePath, destination);
  if (await readText(destination) !== before) throw new Error("move_verification_failed");
  logSystem("Editor", { event: "editor_file_moved", filePath, destinationPath: destination, changed: true });
  return { ok: true, action: "move", filePath, changed: true, message: "file moved", destinationPath: destination };
}

async function applyRename(filePath: string, payload: unknown): Promise<SystemActionResult> {
  const destination = path.join(path.dirname(filePath), renameTargetFromPayload(payload));
  await ensureMissing(destination, "rename");
  const before = await readText(filePath);
  await fs.rename(filePath, destination);
  if (await readText(destination) !== before) throw new Error("rename_verification_failed");
  logSystem("Editor", { event: "editor_file_renamed", filePath, destinationPath: destination, changed: true });
  return { ok: true, action: "rename", filePath, changed: true, message: "file renamed", destinationPath: destination };
}

export async function runSystemAction(manifest: SystemActionManifest): Promise<SystemActionResult> {
  const action = normalizeSystemAction(manifest?.Action) as SystemActionName;
  if (!isSystemAction(action)) throw new Error(`unsupported_system_action:${action}`);

  const abs = resolveFile(manifest.FilePath);
  logSystem("SystemActions", { event: "execute_start", Action: action, FilePath: manifest.FilePath, Task: manifest.Task ?? null });

  switch (action) {
    case "clearfile": return await applyClearFile(abs, manifest.Payload);
    case "delete": return await applyDelete(abs);
    case "copy": return await applyCopy(abs, manifest.Payload);
    case "move": return await applyMove(abs, manifest.Payload);
    case "rename": return await applyRename(abs, manifest.Payload);
    default: throw new Error(`unsupported_system_action:${action}`);
  }
}

export async function runEditorSystemAction(input: EditorSystemActionInput): Promise<SystemActionResult> {
  const action = normalizeSystemAction(input?.action) as SystemActionName;
  if (!isSystemAction(action)) throw new Error(`unsupported_editor_system_action:${action}`);

  const filePath = String(input?.filePath || "").trim();
  if (!filePath) throw new Error("editor_system_action_filepath_missing");

  const abs = resolveFile(filePath);
  logSystem("Editor", { event: "editor_system_action_start", action, filePath });

  switch (action) {
    case "clearfile": return await applyClearFile(abs, input.payload);
    case "delete": return await applyDelete(abs);
    case "copy": return await applyCopy(abs, input.payload);
    case "move": return await applyMove(abs, input.payload);
    case "rename": return await applyRename(abs, input.payload);
    default: throw new Error(`unsupported_editor_system_action:${action}`);
  }
}
