import crypto from "crypto";
import fs from "fs";
import os from "os";
import path from "path";
import { getConfOptions } from "./conf-path.js";
import { deriveToken } from "./fingerprint.js";
import * as api from "./api.js";

const RECOVERY_VERSION = 1;

function getRecoveryDir() {
  const opts = getConfOptions();
  if (opts.cwd) return path.join(opts.cwd, "recovery");
  return path.join(os.homedir(), ".config", "clauth", "recovery");
}

function hashValue(value) {
  return crypto.createHash("sha256").update(String(value), "utf8").digest("hex");
}

function deriveRecoveryKey(password, machineHash, salt) {
  return crypto.scryptSync(`${password}:${machineHash}`, salt, 32);
}

function safeServiceName(service) {
  return String(service || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
}

export function normalizeCredentialValue(value, { keyType = "", service = "" } = {}) {
  if (typeof value !== "string") return value;
  const text = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  const looksPem = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/.test(text);
  const looksSsh = String(keyType).toLowerCase() === "ssh" || /ssh|pem|private/i.test(service);
  if (!looksPem && !looksSsh) return value;
  return text.replace(/\n*$/, "\n");
}

export async function snapshotCredentialBeforeWrite({ password, machineHash, service, logFile }) {
  if (!password || !machineHash || !service) return { ok: false, skipped: "missing_context" };

  let current;
  try {
    const { token, timestamp } = deriveToken(password, machineHash);
    current = await api.retrieve(password, machineHash, token, timestamp, service);
  } catch (err) {
    return { ok: false, skipped: "retrieve_failed", error: err.message };
  }

  if (current?.error || current?.value === undefined || current?.value === null) {
    return { ok: false, skipped: current?.error || "no_existing_value" };
  }

  const value = typeof current.value === "string" ? current.value : JSON.stringify(current.value);
  const now = new Date().toISOString();
  const salt = crypto.randomBytes(16);
  const iv = crypto.randomBytes(12);
  const key = deriveRecoveryKey(password, machineHash, salt);
  const meta = {
    version: RECOVERY_VERSION,
    service,
    key_type: current.key_type || null,
    created_at: now,
    value_sha256: hashValue(value),
  };

  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  cipher.setAAD(Buffer.from(JSON.stringify(meta), "utf8"));
  const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();

  const payload = {
    ...meta,
    kdf: "scrypt",
    cipher: "aes-256-gcm",
    salt: salt.toString("base64"),
    iv: iv.toString("base64"),
    tag: tag.toString("base64"),
    ciphertext: ciphertext.toString("base64"),
  };

  const dir = getRecoveryDir();
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
  const stamp = now.replace(/[-:.]/g, "").replace("T", "-").replace("Z", "");
  const filePath = path.join(dir, `${stamp}-${safeServiceName(service)}.json`);
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });

  if (logFile) {
    try { fs.appendFileSync(logFile, `[${now}] Recovery snapshot written for ${service}: ${filePath}\n`); } catch {}
  }
  return { ok: true, filePath, value_sha256: meta.value_sha256 };
}

export async function writeCredentialWithRecovery({ password, machineHash, service, value, logFile, normalize = true }) {
  const { token, timestamp } = deriveToken(password, machineHash);
  const status = await api.status(password, machineHash, token, timestamp);
  const svc = (status.services || []).find(s => String(s.name || "").toLowerCase() === String(service || "").toLowerCase());
  const normalizedValue = normalize ? normalizeCredentialValue(value, { keyType: svc?.key_type, service }) : value;
  const snapshot = await snapshotCredentialBeforeWrite({ password, machineHash, service, logFile });
  const result = await api.write(password, machineHash, token, timestamp, service, normalizedValue);
  return { result, snapshot, normalized: normalizedValue !== value };
}
