const OPENFGA_MODEL_NAME = "hwlab-v02-openfga-authorization-v1";
const OPENFGA_CONTRACT_VERSION = "hwlab-openfga-authorization-v1";
const DEFAULT_TIMEOUT_MS = 1200;
const SUPPORTED_MODES = new Set(["off", "enforce"]);

export const HWLAB_TOOL_IDS = Object.freeze(["hwpod", "unidesk_ssh", "trans_cmd", "github_pr"]);

export function createOpenFgaAuthorizer(options = {}) {
  return new OpenFgaAuthorizer(options);
}

export function openFgaUser(userId) {
  return `user:${sanitizeObjectId(userId)}`;
}

export function openFgaObject(type, id) {
  return `${sanitizeType(type)}:${sanitizeObjectId(id)}`;
}

export class OpenFgaAuthorizer {
  constructor({ env = process.env, fetchImpl = fetch, now = () => new Date().toISOString(), configStore = null } = {}) {
    this.env = env;
    this.fetchImpl = fetchImpl;
    this.now = now;
    this.configStore = configStore;
    this.mode = normalizeMode(env.HWLAB_OPENFGA_MODE);
    this.apiUrl = normalizeBaseUrl(env.HWLAB_OPENFGA_API_URL);
    this.token = textOr(env.HWLAB_OPENFGA_AUTHN_TOKEN ?? env.OPENFGA_API_TOKEN, "");
    this.storeName = textOr(env.HWLAB_OPENFGA_STORE_NAME, "hwlab-v02");
    this.timeoutMs = boundedTimeout(env.HWLAB_OPENFGA_TIMEOUT_MS);
    this.bootstrapEnabled = textOr(env.HWLAB_OPENFGA_BOOTSTRAP, "1") !== "0";
    this.storeId = textOr(env.HWLAB_OPENFGA_STORE_ID ?? env.FGA_STORE_ID, "");
    this.modelId = textOr(env.HWLAB_OPENFGA_MODEL_ID ?? env.FGA_MODEL_ID, "");
    this.readyCache = null;
  }

  async describe({ refresh = false } = {}) {
    const readiness = await this.ensureReady({ refresh });
    return {
      contractVersion: OPENFGA_CONTRACT_VERSION,
      mode: this.mode,
      configured: Boolean(this.apiUrl),
      ready: readiness.ready,
      status: readiness.ready ? "ready" : this.mode === "off" ? "disabled" : "degraded",
      apiUrlConfigured: Boolean(this.apiUrl),
      storeName: this.storeName,
      storeId: redactedId(readiness.storeId || this.storeId),
      modelId: redactedId(readiness.modelId || this.modelId),
      degradedReason: readiness.degradedReason,
      valuesRedacted: true
    };
  }

  async check({ actor = null, userId = "", relation = "", object = "" } = {}) {
    const actorId = textOr(userId, actor?.id ?? "");
    const rel = textOr(relation, "");
    const obj = textOr(object, "");
    if (!actorId || !rel || !obj) return decision({ mode: this.mode, allowed: false, decisionSource: "invalid-request", relation: rel, object: obj, userId: actorId });
    if (this.mode === "off") return decision({ mode: this.mode, allowed: false, decisionSource: "openfga-disabled", relation: rel, object: obj, userId: actorId, degradedReason: "openfga_disabled" });

    const readiness = await this.ensureReady();
    if (!readiness.ready) {
      return decision({
        mode: this.mode,
        allowed: false,
        fgaAllowed: null,
        decisionSource: "openfga-degraded",
        relation: rel,
        object: obj,
        userId: actorId,
        degradedReason: readiness.degradedReason
      });
    }

    const fga = await this.checkOpenFga({ user: openFgaUser(actorId), relation: rel, object: obj, readiness });
    if (fga.allowed) {
      return decision({
        mode: this.mode,
        allowed: true,
        fgaAllowed: true,
        decisionSource: "openfga",
        relation: rel,
        object: obj,
        userId: actorId,
        storeId: readiness.storeId,
        modelId: readiness.modelId,
        degradedReason: null
      });
    }
    const systemAdminObject = openFgaObject("system", "hwlab");
    if (!(rel === "admin" && obj === systemAdminObject)) {
      const admin = await this.checkOpenFga({ user: openFgaUser(actorId), relation: "admin", object: systemAdminObject, readiness });
      if (admin.allowed) {
        return decision({
          mode: this.mode,
          allowed: true,
          fgaAllowed: true,
          decisionSource: "openfga-admin",
          relation: rel,
          object: obj,
          userId: actorId,
          storeId: readiness.storeId,
          modelId: readiness.modelId,
          degradedReason: null
        });
      }
    }
    return decision({
      mode: this.mode,
      allowed: false,
      fgaAllowed: false,
      decisionSource: "openfga",
      relation: rel,
      object: obj,
      userId: actorId,
      storeId: readiness.storeId,
      modelId: readiness.modelId,
      degradedReason: fga.error ?? null
    });
  }

  async writeTuple({ userId, relation, object } = {}) {
    return this.writeTuples({ writes: [{ user: openFgaUser(userId), relation: textOr(relation, ""), object: textOr(object, "") }] });
  }

  async deleteTuple({ userId, relation, object } = {}) {
    return this.writeTuples({ deletes: [{ user: openFgaUser(userId), relation: textOr(relation, ""), object: textOr(object, "") }] });
  }

  async writeTuples({ writes = [], deletes = [] } = {}) {
    const sanitizedWrites = writes.map(sanitizeTuple).filter(Boolean);
    const sanitizedDeletes = deletes.map(sanitizeTuple).filter(Boolean);
    if (this.mode === "off") {
      return { ok: false, mode: this.mode, status: 503, error: { code: "openfga_disabled", message: "OpenFGA is disabled" }, written: 0, deleted: 0 };
    }
    const readiness = await this.ensureReady();
    if (!readiness.ready) {
      return { ok: false, mode: this.mode, status: 503, error: { code: "openfga_degraded", message: readiness.degradedReason }, written: 0, deleted: 0 };
    }
    const body = {
      ...(sanitizedWrites.length > 0 ? { writes: { tuple_keys: sanitizedWrites, on_duplicate: "ignore" } } : {}),
      ...(sanitizedDeletes.length > 0 ? { deletes: { tuple_keys: sanitizedDeletes } } : {}),
      authorization_model_id: readiness.modelId
    };
    const result = await this.request(`/stores/${encodeURIComponent(readiness.storeId)}/write`, { method: "POST", body });
    return {
      ok: result.ok,
      mode: this.mode,
      status: result.status,
      written: sanitizedWrites.length,
      deleted: sanitizedDeletes.length,
      storeId: redactedId(readiness.storeId),
      modelId: redactedId(readiness.modelId),
      error: result.ok ? null : { code: "openfga_write_failed", message: result.message }
    };
  }

  async ensureReady({ refresh = false } = {}) {
    if (this.mode === "off") return { ready: false, storeId: this.storeId, modelId: this.modelId, degradedReason: "openfga_disabled" };
    if (!this.apiUrl) return { ready: false, storeId: "", modelId: "", degradedReason: "openfga_api_url_missing" };
    if (this.readyCache && !refresh) return this.readyCache;

    const health = await this.request("/healthz", { method: "GET", allowNotJson: true });
    if (!health.ok) return this.cacheReady({ ready: false, storeId: this.storeId, modelId: this.modelId, degradedReason: `openfga_health_failed:${health.status}` });

    let storeId = this.storeId || await this.getConfigValue("openfga.storeId");
    if (!storeId && this.bootstrapEnabled) {
      const created = await this.request("/stores", { method: "POST", body: { name: this.storeName } });
      if (created.ok) {
        storeId = textOr(created.body?.id, "");
        if (storeId) await this.setConfigValue("openfga.storeId", storeId);
      }
    }
    if (!storeId) return this.cacheReady({ ready: false, storeId: "", modelId: "", degradedReason: "openfga_store_id_missing" });
    this.storeId = storeId;

    let modelId = this.modelId || await this.getConfigValue("openfga.modelId");
    if (!modelId && this.bootstrapEnabled) {
      const written = await this.request(`/stores/${encodeURIComponent(storeId)}/authorization-models`, { method: "POST", body: HWLAB_AUTHORIZATION_MODEL });
      if (written.ok) {
        modelId = textOr(written.body?.authorization_model_id ?? written.body?.id, "");
        if (modelId) await this.setConfigValue("openfga.modelId", modelId);
      } else {
        return this.cacheReady({ ready: false, storeId, modelId: "", degradedReason: `openfga_model_write_failed:${written.status}` });
      }
    }
    if (!modelId) return this.cacheReady({ ready: false, storeId, modelId: "", degradedReason: "openfga_model_id_missing" });
    this.modelId = modelId;
    return this.cacheReady({ ready: true, storeId, modelId, degradedReason: null });
  }

  cacheReady(value) {
    this.readyCache = value;
    return value;
  }

  async checkOpenFga({ user, relation, object, readiness }) {
    const result = await this.request(`/stores/${encodeURIComponent(readiness.storeId)}/check`, {
      method: "POST",
      body: { tuple_key: { user, relation, object }, authorization_model_id: readiness.modelId }
    });
    if (!result.ok) return { allowed: false, error: `openfga_check_failed:${result.status}` };
    return { allowed: result.body?.allowed === true, error: null };
  }

  async request(path, { method = "GET", body = null, allowNotJson = false } = {}) {
    const headers = { accept: "application/json" };
    if (body !== null) headers["content-type"] = "application/json";
    if (this.token) headers.authorization = `Bearer ${this.token}`;
    try {
      const response = await fetchWithTimeout(this.fetchImpl, `${this.apiUrl}${path}`, {
        method,
        headers,
        ...(body !== null ? { body: JSON.stringify(body) } : {})
      }, this.timeoutMs);
      const text = await response.text();
      const json = allowNotJson ? parseJson(text, {}) : parseJson(text, {});
      return { ok: response.status < 400, status: response.status, body: json, message: textOr(json?.message ?? json?.error_description ?? json?.error, text) };
    } catch (error) {
      return { ok: false, status: 0, body: {}, message: error?.message ?? String(error) };
    }
  }

  async getConfigValue(key) {
    return textOr(await this.configStore?.getAccessConfig?.(key), "");
  }

  async setConfigValue(key, value) {
    await this.configStore?.setAccessConfig?.({ key, value, updatedAt: this.now() });
  }
}

const HWLAB_AUTHORIZATION_MODEL = Object.freeze({
  schema_version: "1.1",
  type_definitions: [
    { type: "user" },
    directTypeDefinition("system", ["admin", "access_manager", "can_manage_users", "can_manage_tools"]),
    directTypeDefinition("agent_session", ["owner", "viewer", "operator"]),
    directTypeDefinition("tool", ["can_use"])
  ]
});

function directTypeDefinition(type, relations) {
  return {
    type,
    relations: Object.fromEntries(relations.map((relation) => [relation, { this: {} }])),
    metadata: {
      relations: Object.fromEntries(relations.map((relation) => [relation, { directly_related_user_types: [{ type: "user" }] }]))
    }
  };
}

function decision(input = {}) {
  return {
    contractVersion: OPENFGA_CONTRACT_VERSION,
    mode: input.mode,
    allowed: input.allowed === true,
    fgaAllowed: input.fgaAllowed ?? null,
    mismatch: input.mismatch === true,
    decisionSource: textOr(input.decisionSource, "unknown"),
    userId: textOr(input.userId, ""),
    relation: textOr(input.relation, ""),
    object: textOr(input.object, ""),
    storeId: redactedId(input.storeId),
    modelId: redactedId(input.modelId),
    degradedReason: input.degradedReason ?? null,
    valuesRedacted: true
  };
}

function sanitizeTuple(tuple = {}) {
  const user = textOr(tuple.user, "");
  const relation = textOr(tuple.relation, "");
  const object = textOr(tuple.object, "");
  if (!user || !relation || !object) return null;
  return { user, relation, object };
}

function normalizeMode(value) {
  const mode = textOr(value, "off").toLowerCase();
  if (mode === "shadow") return "enforce";
  return SUPPORTED_MODES.has(mode) ? mode : "off";
}

function sanitizeType(value) {
  return textOr(value, "object").replace(/[^A-Za-z0-9_]/gu, "_");
}

function sanitizeObjectId(value) {
  return textOr(value, "unknown").replace(/[^A-Za-z0-9_.:-]/gu, "_");
}

function normalizeBaseUrl(value) {
  const text = textOr(value, "").replace(/\/+$/u, "");
  return /^https?:\/\//u.test(text) ? text : "";
}

function boundedTimeout(value) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Math.min(Math.max(Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS, 100), 10000);
}

function redactedId(value) {
  const text = textOr(value, "");
  if (!text) return null;
  if (text.length <= 12) return text;
  return `${text.slice(0, 8)}...${text.slice(-4)}`;
}

function textOr(value, fallback) {
  const text = String(value ?? "").trim();
  return text || fallback;
}

function parseJson(value, fallback) {
  if (!value) return fallback;
  if (typeof value === "object") return value;
  try { return JSON.parse(String(value)); } catch { return fallback; }
}

async function fetchWithTimeout(fetchImpl, url, options, timeoutMs) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetchImpl(url, { ...options, signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
}

