"use strict";

const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");

class WorkOrderManager {
  constructor({ approvalTtl = 15 * 60, sessionTtl = 60 * 60, stateFile = "" } = {}) {
    this.approvalTtl = approvalTtl;
    this.sessionTtl = sessionTtl;
    this.stateFile = stateFile;
    this.workorders = new Map();
    this.sessions = new Map();
    this.load();
  }

  request({ persona, target, scope, action, allowedActions = [action], description = "" }, now = Date.now() / 1000) {
    const id = crypto.randomUUID();
    const approvalToken = randomToken();
    const claimToken = randomToken();
    this.workorders.set(id, {
      id,
      persona: { pid: persona.pid, name: persona.name || persona.pid },
      target,
      scope,
      action,
      allowedActions: [...new Set(allowedActions)],
      description: String(description).slice(0, 500),
      createdAt: now,
      expiresAt: now + this.approvalTtl,
      approvalHash: hash(approvalToken),
      claimHash: hash(claimToken),
      state: "pending",
      claimed: false,
    });
    this.persist();
    return { id, approvalToken, claimToken, expiresIn: this.approvalTtl };
  }

  inspectApproval(approvalToken, now = Date.now() / 1000) {
    const order = this.findByApproval(approvalToken);
    if (!order) return { ok: false, reason: "approval_not_found" };
    if (now > order.expiresAt) return { ok: false, reason: "approval_expired" };
    if (order.state !== "pending") return { ok: false, reason: "approval_already_used" };
    return { ok: true, order: publicOrder(order) };
  }

  approve(approvalToken, now = Date.now() / 1000) {
    const inspected = this.inspectApproval(approvalToken, now);
    if (!inspected.ok) return inspected;
    const order = this.workorders.get(inspected.order.id);
    order.state = "approved";
    order.approvedAt = now;
    order.approvalHash = "";
    this.persist();
    return { ok: true, order: publicOrder(order) };
  }

  claim(id, claimToken, now = Date.now() / 1000) {
    const order = this.workorders.get(id);
    if (!order || !safeEqual(order.claimHash, hash(claimToken || ""))) return { ok: false, reason: "claim_not_found" };
    if (now > order.expiresAt) return { ok: false, reason: "approval_expired" };
    if (order.state !== "approved") return { ok: false, reason: order.state === "pending" ? "approval_pending" : "claim_unavailable" };
    if (order.claimed) return { ok: false, reason: "claim_already_used" };

    const sessionToken = randomToken();
    this.sessions.set(hash(sessionToken), {
      persona: order.persona,
      target: order.target,
      scope: order.scope,
      action: order.action,
      actions: order.allowedActions || [order.action],
      createdAt: now,
      expiresAt: now + this.sessionTtl,
    });
    order.claimed = true;
    order.state = "claimed";
    order.claimHash = "";
    this.persist();
    return { ok: true, sessionToken, expiresIn: this.sessionTtl, target: order.target, scope: order.scope, action: order.action };
  }

  verifySession(sessionToken, persona, target, scope, action, now = Date.now() / 1000) {
    const key = hash(sessionToken || "");
    const session = this.sessions.get(key);
    if (!session) return { ok: false, reason: "session_not_found" };
    if (now > session.expiresAt) {
      this.sessions.delete(key);
      this.persist();
      return { ok: false, reason: "session_expired" };
    }
    if (session.persona.pid !== persona.pid) return { ok: false, reason: "persona_mismatch" };
    if (session.target !== target) return { ok: false, reason: "target_mismatch" };
    if (session.scope !== scope) return { ok: false, reason: "scope_mismatch" };
    if (!(session.actions || [session.action]).includes(action)) return { ok: false, reason: "action_mismatch" };
    return { ok: true, session: { ...session } };
  }

  findByApproval(token) {
    const needle = hash(token || "");
    for (const order of this.workorders.values()) {
      if (order.approvalHash && safeEqual(order.approvalHash, needle)) return order;
    }
    return null;
  }

  load(now = Date.now() / 1000) {
    if (!this.stateFile || !fs.existsSync(this.stateFile)) return;
    try {
      const state = JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
      for (const order of state.workorders || []) {
        if (order.expiresAt >= now) this.workorders.set(order.id, order);
      }
      for (const [tokenHash, session] of Object.entries(state.sessions || {})) {
        if (session.expiresAt >= now) this.sessions.set(tokenHash, session);
      }
    } catch {
      this.workorders.clear();
      this.sessions.clear();
    }
  }

  persist() {
    if (!this.stateFile) return;
    const stateDir = path.dirname(this.stateFile);
    if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
    const temp = `${this.stateFile}.${process.pid}.tmp`;
    fs.writeFileSync(temp, JSON.stringify({ version: 1, workorders: [...this.workorders.values()], sessions: Object.fromEntries(this.sessions) }), { mode: 0o600 });
    fs.renameSync(temp, this.stateFile);
  }
}

function randomToken() {
  return crypto.randomBytes(32).toString("base64url");
}

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

function safeEqual(left, right) {
  const a = Buffer.from(String(left));
  const b = Buffer.from(String(right));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function publicOrder(order) {
  return {
    id: order.id,
    persona: { ...order.persona },
    target: order.target,
    scope: order.scope,
    action: order.action,
    description: order.description,
    createdAt: order.createdAt,
    expiresAt: order.expiresAt,
    state: order.state,
  };
}

module.exports = { WorkOrderManager };
