import fs from "node:fs";
import path from "node:path";
import { z } from "zod";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { expandUserPath } from "../utils/path-normalization.js";

export const NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const;
export type NexusMode = (typeof NEXUS_MODES)[number];

export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const;
export type VoiceMode = (typeof VOICE_MODES)[number];

const paletteRoleSchema = z.object({
  name: z.string(),
  dark: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }),
  light: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }),
});

export const nexusSettingsSchema = z.object({
  mode: z.enum(NEXUS_MODES).default("both"),
  voiceEnabled: z.boolean().default(false),
  voiceMode: z.enum(VOICE_MODES).default("text"),
  telegramToken: z.string().optional(),
  piperBinaryPath: z.string().optional(),
  whisperBinaryPath: z.string().optional(),
  customTheme: z
    .object({
      seedHex: z.string(),
      palette: z.array(paletteRoleSchema),
    })
    .optional(),
});

type NexusSettings = z.infer<typeof nexusSettingsSchema>;

function resolveNexusSettingsPath(): string {
  return path.resolve(resolvePaperclipInstanceRoot(), "data", "nexus-settings.json");
}

export function nexusSettingsService() {
  async function get(): Promise<NexusSettings> {
    const filePath = resolveNexusSettingsPath();
    try {
      const raw = fs.readFileSync(filePath, "utf-8");
      const parsed = nexusSettingsSchema.safeParse(JSON.parse(raw));
      if (parsed.success) {
        return parsed.data;
      }
      return nexusSettingsSchema.parse({});
    } catch {
      return nexusSettingsSchema.parse({});
    }
  }

  async function set(patch: Partial<NexusSettings>): Promise<NexusSettings> {
    const current = await get();
    // [nexus] Zero-terminal: expand ~ in user-supplied binary paths before
    // persisting. These are executable paths (voice pipeline binaries), so
    // we expand but don't mkdir — caller still has to install the binary
    // itself, but at least the stored path is resolvable by the server.
    const pathNormalizedPatch: Partial<NexusSettings> = { ...patch };
    if (typeof patch.piperBinaryPath === "string" && patch.piperBinaryPath.trim().length > 0) {
      pathNormalizedPatch.piperBinaryPath = expandUserPath(patch.piperBinaryPath) ?? patch.piperBinaryPath;
    }
    if (typeof patch.whisperBinaryPath === "string" && patch.whisperBinaryPath.trim().length > 0) {
      pathNormalizedPatch.whisperBinaryPath = expandUserPath(patch.whisperBinaryPath) ?? patch.whisperBinaryPath;
    }
    const merged = { ...current, ...pathNormalizedPatch };
    // Validate — will throw ZodError if invalid
    const validated = nexusSettingsSchema.parse(merged);

    const filePath = resolveNexusSettingsPath();
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify(validated, null, 2), "utf-8");
    return validated;
  }

  return { get, set };
}
