// controller.ts
// Controller owns local runtime project state, Backstage mode, project rails, and path enforcement.
// Server owns licensing, usage, limits, package rules, and run permission.

import path from "node:path";
import { projectLookup, type ProjectRecord } from "./mgrSNP";
import { ROOT, DataPaths, SYSTEM_PROJECT_META_PATH, SYSTEM_MESSAGES_PATH, isInsideRoot } from "./pathList";

export const BACKSTAGE_SERIAL = "system";

export type AccessTier = "backstage" | "unlimited" | "limited" | "";
export type ProjectMode = "system" | "project_select" | "project";

export type Guardrails = {
  systemGuardrails: boolean;
  manifestScrambler: boolean;
  extensionCapable: boolean;
};

export type ControllerState = {
  activeProject: string;
  projectSerial: string;
  accessTier: AccessTier;
  projectMode: ProjectMode;
  guardrails: Guardrails;
};

export type ProjectRails = {
  ok: boolean;
  serial: string;
  root: string;
  isBackstage: boolean;
  error?: string;
  message?: string;
};

export type RailsCheck = {
  ok: boolean;
  error?: string;
  message?: string;
  state: ReturnType<typeof getControllerSummaryState>;
  rails?: ProjectRails;
};

const LOCAL_GUARDRAILS: Guardrails = {
  systemGuardrails: true,
  manifestScrambler: false,
  extensionCapable: false
};

const EMPTY_CONTROLLER_STATE: ControllerState = {
  activeProject: "",
  projectSerial: "",
  accessTier: "",
  projectMode: "project_select",
  guardrails: { ...LOCAL_GUARDRAILS }
};

let controllerState: ControllerState = cloneState(EMPTY_CONTROLLER_STATE);

function cloneState<T>(value: T): T {
  return JSON.parse(JSON.stringify(value));
}

function normalizeAccessTier(value: unknown): AccessTier {
  const tier = String(value || "").trim().toLowerCase();
  if (tier === "backstage") return "backstage";
  if (tier === "unlimited" || tier === "standard" || tier === "custom" || tier === "lifetime_custom") return "unlimited";
  if (tier === "limited") return "limited";
  return "";
}

function readVerbatimVersion(): string {
  try {
    const pkg = JSON.parse(require("node:fs").readFileSync(path.join(ROOT, "package.json"), "utf8"));
    return String(pkg?.version || "0.0.0").trim() || "0.0.0";
  } catch {
    return "0.0.0";
  }
}

function verbatimWelcomeName(): string {
  return `Nodarama Verbatim V. ${readVerbatimVersion()}`;
}

function clearControllerState(): void {
  controllerState = cloneState(EMPTY_CONTROLLER_STATE);
}

function setControllerAccessTier(tier: string): ControllerState {
  const accessTier = normalizeAccessTier(tier) || "limited";
  const wasSystemScope = controllerState.projectSerial === BACKSTAGE_SERIAL || controllerState.projectMode === "system";

  controllerState = {
    ...controllerState,
    accessTier,
    ...(accessTier !== "backstage" && wasSystemScope ? {
      activeProject: "",
      projectSerial: "",
      projectMode: "project_select" as ProjectMode
    } : {})
  };

  return getControllerState();
}

function setControllerActiveProject(name: string, serial: string): ControllerState {
  const cleanSerial = String(serial || "").trim();
  const isSystem = cleanSerial === BACKSTAGE_SERIAL;

  controllerState = {
    ...controllerState,
    activeProject: isSystem ? "System" : String(name || "").trim(),
    projectSerial: cleanSerial,
    projectMode: isSystem ? "system" : cleanSerial ? "project" : "project_select"
  };

  return getControllerState();
}

function clearControllerActiveProject(): ControllerState {
  controllerState = {
    ...controllerState,
    activeProject: "",
    projectSerial: "",
    projectMode: "project_select"
  };

  return getControllerState();
}

function getControllerState(): ControllerState {
  return cloneState(controllerState);
}

function getAccessTier(): AccessTier {
  return controllerState.accessTier;
}

function getProjectSerial(): string {
  return controllerState.projectSerial;
}

function isBackstage(): boolean {
  return controllerState.accessTier === "backstage" &&
    (controllerState.projectSerial === BACKSTAGE_SERIAL || controllerState.projectMode === "system");
}

function requiresProjectSelection(): boolean {
  return controllerState.projectMode === "project_select";
}

function hasSelectedProject(): boolean {
  return isBackstage() || Boolean(controllerState.projectSerial);
}

function getGuardrails(): Guardrails {
  return { ...controllerState.guardrails };
}

function getControllerSummaryState() {
  return {
    ...getControllerState(),
    projectRequired: requiresProjectSelection(),
    isBackstage: isBackstage()
  };
}

function projectRoot(project: ProjectRecord): string {
  const loc = String(project.location || project.path || "").trim();
  if (!loc) throw new Error(`project_root_not_found:${project.serialNo}`);
  return path.isAbsolute(loc) ? path.resolve(loc) : path.resolve(ROOT, loc);
}

function findProjectBySerial(serial: string): ProjectRecord | null {
  const clean = String(serial || "").trim();
  if (!clean || clean === BACKSTAGE_SERIAL) return null;
  return projectLookup({ serialNo: clean }).project;
}

function okProjectRails(serial: string, root: string, backstage: boolean): ProjectRails {
  return { ok: true, serial, root: path.resolve(root), isBackstage: backstage };
}

function blockedProjectRails(error: string, message?: string, serial = ""): ProjectRails {
  return { ok: false, serial, root: "", isBackstage: false, error, message };
}

function selectedProjectRoot(serial = getProjectSerial() || BACKSTAGE_SERIAL): string {
  const clean = String(serial || "").trim() || BACKSTAGE_SERIAL;
  if (clean === BACKSTAGE_SERIAL) return path.resolve(ROOT);

  const project = findProjectBySerial(clean);
  if (!project) throw new Error(`project_root_not_found:${clean}`);
  return projectRoot(project);
}

function currentProjectRails(): ProjectRails {
  if (isBackstage()) return okProjectRails(BACKSTAGE_SERIAL, ROOT, true);

  if (controllerState.projectSerial === BACKSTAGE_SERIAL || controllerState.projectMode === "system") {
    return blockedProjectRails("system_requires_backstage", "System is available only to Backstage users.", BACKSTAGE_SERIAL);
  }

  if (!hasSelectedProject() || requiresProjectSelection()) {
    return blockedProjectRails("project_required_for_writes", "Select a project before recording changes.");
  }

  const serial = getProjectSerial();
  try { return okProjectRails(serial, selectedProjectRoot(serial), false); }
  catch (err: any) { return blockedProjectRails(err?.message ?? String(err), undefined, serial); }
}

function normalizeProjectFilePath(filePath: string): string {
  return String(filePath || "").trim().replace(/\\/g, "/");
}

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

export const controller = {
  boot: {
    clear: clearControllerState,
    tier: setControllerAccessTier,
    project: setControllerActiveProject,
    clearProject: clearControllerActiveProject,
    backstage() {
      setControllerAccessTier("backstage");
      return setControllerActiveProject("System", BACKSTAGE_SERIAL);
    }
  },

  activeProjectInquiry() {
    const state = getControllerSummaryState();
    const rails = currentProjectRails();
    const isSystemMessages = !state.isBackstage && state.projectMode !== "project";
    const displayProject = isSystemMessages ? verbatimWelcomeName() : state.activeProject;

    return {
      ok: rails.ok,
      activeProject: displayProject,
      projectName: displayProject,
      projectSerial: isSystemMessages ? "" : state.projectSerial,
      projectRoot: rails.ok ? rails.root : "",
      isBackstage: state.isBackstage,
      projectMode: isSystemMessages ? "project_select" : state.projectMode,
      systemMessages: isSystemMessages,
      error: rails.error,
      message: rails.message
    };
  },

  setActiveProject(name: string, serial: string) {
    return setControllerActiveProject(name, serial);
  },

  initProject(input: { name?: string; serialNo?: string; key?: string; projectKey?: string; identifier?: string; location?: string; path?: string; projectFile?: string; source?: string } = {}) {
    return controller.initiateProject(input);
  },

  state() {
    return getControllerSummaryState();
  },

  selectedProjectRoot,
  currentProjectRails,

  validateRails(): RailsCheck {
    const state = getControllerSummaryState();
    const rails = currentProjectRails();
    if (!rails.ok) return { ok: false, error: rails.error, message: rails.message, state, rails };
    return { ok: true, state, rails };
  },

  resolveProjectPath(filePath: string): string {
    const clean = normalizeProjectFilePath(filePath);
    if (!clean) throw new Error("recorder_filepath_missing");

    const rails = currentProjectRails();
    if (!rails.ok) throw new Error(rails.error || "project_rails_blocked");

    const relative = clean.replace(/^\/+/, "");
    const abs = isExplicitAbsolutePath(clean) ? path.resolve(clean) : path.resolve(rails.root, relative);
    controller.assertInsideProjectRails(abs);
    return abs;
  },

  assertInsideProjectRails(absPath: string): void {
    const rails = currentProjectRails();
    if (!rails.ok) throw new Error(rails.error || "project_rails_blocked");
    if (!isInsideRoot(path.resolve(absPath), rails.root)) {
      throw new Error(rails.isBackstage ? "path_outside_nv_root" : "outside_project_rails");
    }
  },

  projectForPath(absPath: string): ProjectRecord | null {
    const abs = path.resolve(absPath);
    let best: { project: ProjectRecord; rootLen: number } | null = null;

    for (const project of projectLookup({ all: true }).projects) {
      const root = projectRoot(project);
      if (!isInsideRoot(abs, root)) continue;
      if (!best || root.length > best.rootLen) best = { project, rootLen: root.length };
    }

    return best?.project || null;
  },

  async withProjectForPath<T>(absPath: string, fn: (projectContext: { serial: string; name: string; switched: boolean; root: string }) => Promise<T>): Promise<T> {
    const before = getControllerState();
    const project = controller.projectForPath(absPath);

    if (!isBackstage() || !project) {
      const fallbackSerial = before.projectSerial && before.projectMode === "project" ? before.projectSerial : "";
      return await fn({
        serial: fallbackSerial,
        name: fallbackSerial ? before.activeProject : verbatimWelcomeName(),
        switched: false,
        root: fallbackSerial ? selectedProjectRoot(fallbackSerial) : ""
      });
    }

    const serial = String(project.serialNo || "").trim();
    const name = String(project.name || "").trim();
    const root = projectRoot(project);

    setControllerActiveProject(name, serial);
    try { return await fn({ serial, name, switched: true, root }); }
    finally { setControllerActiveProject(before.activeProject || "System", before.projectSerial || BACKSTAGE_SERIAL); }
  },

  initiateProject(input: { name?: string; serialNo?: string; key?: string; projectKey?: string; identifier?: string; location?: string; path?: string; projectFile?: string; source?: string } = {}) {
    const source = String(input.source || "unknown").trim() || "unknown";
    const keyName = String(input.name || "").trim();
    const keyNameLower = keyName.toLowerCase();
    const keySerial = String(input.serialNo || "").trim().toLowerCase();
    const keyAny = String(input.key || input.projectKey || input.identifier || "").trim();
    const keyAnyLower = keyAny.toLowerCase();
    const keyLocation = String(input.location || "").trim().replace(/\\/g, "/").toLowerCase();
    const keyPath = String(input.path || "").trim().replace(/\\/g, "/").toLowerCase();
    const keyProjectFile = String(input.projectFile || "").trim().replace(/\\/g, "/").toLowerCase();

    if (keySerial === BACKSTAGE_SERIAL || keyAnyLower === BACKSTAGE_SERIAL || keyNameLower === "system" || keyAnyLower === "system") {
      if (getAccessTier() !== "backstage") {
        return { ok: false, switched: false, source, error: "system_requires_backstage", message: "System is available only to Backstage users." };
      }
      controller.boot.backstage();
      const projectState = controller.respondProject();
      return { ok: true, switched: true, source, activeProject: "System", projectSerial: BACKSTAGE_SERIAL, isBackstage: true, projectState };
    }

    if (keyNameLower === "backstage" || keyAnyLower === "backstage") {
      return { ok: false, switched: false, source, error: "backstage_is_access_tier", message: "Backstage is an access tier, not a project." };
    }

    const projects = projectLookup({ all: true }).projects;
    const match = projects.find((project: any) => {
      const serial = String(project.serialNo || "").trim().toLowerCase();
      const name = String(project.name || "").trim();
      const nameLower = name.toLowerCase();
      const location = String(project.location || "").trim().replace(/\\/g, "/").toLowerCase();
      const projectPath = String(project.path || "").trim().replace(/\\/g, "/").toLowerCase();
      const projectFile = String(project.projectFile || "").trim().replace(/\\/g, "/").toLowerCase();

      return Boolean(
        (keySerial && serial === keySerial) ||
        (keyName && name === keyName) ||
        (keyNameLower && nameLower === keyNameLower) ||
        (keyAnyLower && (serial === keyAnyLower || nameLower === keyAnyLower || location === keyAnyLower || projectPath === keyAnyLower || projectFile === keyAnyLower)) ||
        (keyLocation && (location === keyLocation || projectPath === keyLocation)) ||
        (keyPath && (location === keyPath || projectPath === keyPath)) ||
        (keyProjectFile && projectFile === keyProjectFile)
      );
    }) || null;

    if (!match) return { ok: false, switched: false, source, error: "unknown_project", message: "Project not found." };

    const name = String(match.name || "").trim();
    const serial = String(match.serialNo || "").trim();
    setControllerActiveProject(name, serial);
    const projectState = controller.respondProject();

    return { ok: true, switched: true, source, activeProject: name, projectSerial: serial, isBackstage: false, project: match, projectState };
  },

  respondProject(): { ok: true; isBackstage: boolean; name: string; summary: string; source: string; accentIndex: number; color: { name: string; primary: string; dark: string; light: string } } {
    const colors = [
      { name: "orange", primary: "#e05c35", dark: "rgba(224, 92, 53, .20)", light: "#ffb39e" },
      { name: "yellow", primary: "#c9db00", dark: "rgba(180, 190, 0, .18)", light: "#efff7a" },
      { name: "green", primary: "#35a152", dark: "rgba(35, 130, 60, .20)", light: "#a4f0b8" },
      { name: "blue", primary: "#00a3fb", dark: "rgba(0, 130, 190, .20)", light: "#a8e7ff" }
    ];

    if (isBackstage()) {
      let systemJson: any = {};
      try { systemJson = JSON.parse(require("node:fs").readFileSync(SYSTEM_MESSAGES_PATH, "utf8")); } catch {}
      const summary = String(systemJson.summary || systemJson.Summary || "News, updates, known issues, workarounds, and getting-started notes appear here before a project is selected.").trim();
      return { ok: true, isBackstage: true, name: "System", summary, source: "system-messages", accentIndex: 0, color: { name: "system", primary: "#aeb8bf", dark: "rgba(130, 145, 150, .22)", light: "#eef6f8" } };
    }

    const state = getControllerState();
    if (state.projectMode !== "project" || !state.projectSerial) {
      let systemJson: any = {};
      try { systemJson = JSON.parse(require("node:fs").readFileSync(SYSTEM_MESSAGES_PATH, "utf8")); } catch {}
      const summary = String(systemJson.summary || systemJson.Summary || "News, updates, known issues, workarounds, and getting-started notes appear here before a project is selected.").trim();
      return { ok: true, isBackstage: false, name: verbatimWelcomeName(), summary, source: "system-messages", accentIndex: 0, color: { name: "system-messages", primary: "#aeb8bf", dark: "rgba(130, 145, 150, .22)", light: "#eef6f8" } };
    }

    const projects = projectLookup({ all: true }).projects;
    const serial = getProjectSerial();
    const active = projects.find((p: any) => String(p.serialNo || "") === String(serial || "")) || null;
    const index = Math.max(0, projects.findIndex((p: any) => String(p.serialNo || "") === String(active?.serialNo || "")));
    const color = colors[index % colors.length];

    let projectJson: any = {};
    try {
      const candidates = active ? [path.join(projectRoot(active), "project.json")] : [];
      for (const candidate of candidates) {
        try {
          const raw = require("node:fs").readFileSync(candidate, "utf8");
          projectJson = JSON.parse(raw);
          break;
        } catch {}
      }
    } catch {}

    const name = String(projectJson.name || projectJson.Name || active?.name || "No Project").trim();
    const summary = String(projectJson.summary || projectJson.Summary || active?.summary || active?.description || "Select a project to begin.").trim();
    return { ok: true, isBackstage: false, name, summary, source: "project", accentIndex: index, color };
  },

  summary() {
    return {
      state: getControllerState(),
      accessTier: getAccessTier(),
      guardrails: getGuardrails(),
      projectRails: currentProjectRails()
    };
  },

  paths: { root: ROOT, projects: DataPaths.projectsDir, logs: DataPaths.logsDir }
};

export const assertWriteAllowed = controller.assertInsideProjectRails;
export const validateRailsNow = controller.validateRails;
