import os from "node:os";
import path from "node:path";

import { CAPABILITY_REGISTRY, executeCapability } from "./capabilities.mjs";
import { createNativeConsentBroker } from "./consent-broker.mjs";
import { assertProfile, permits } from "./profile-policy.mjs";

const SESSION_ROTATE_MARGIN_MS = 60_000;

export function createHostedSession({ token, expiresAt }) {
  if (typeof token !== "string" || token.length < 32) throw new Error("A valid hosted device session is required.");
  const expiry = Date.parse(expiresAt);
  if (!Number.isFinite(expiry)) throw new Error("The hosted device session expiry is invalid.");
  return { token, expiresAt: new Date(expiry).toISOString() };
}

export async function rotateHostedSessionIfNeeded({ bridgeUrl, session, fetchImpl = fetch, now = () => Date.now() }) {
  if (Date.parse(session.expiresAt) - now() > SESSION_ROTATE_MARGIN_MS) return session;
  const response = await deviceRequest({
    bridgeUrl,
    deviceSession: session.token,
    pathname: "/v1/device/sessions/rotate",
    body: {},
    fetchImpl,
  });
  return createHostedSession({ token: response.deviceSession, expiresAt: response.expiresAt });
}

export async function runHostedOnce(options) {
  const bridgeUrl = new URL(options.bridgeUrl);
  if (bridgeUrl.protocol !== "https:" && !isLoopbackHttp(bridgeUrl)) {
    throw new Error("The hosted Talons bridge must use HTTPS outside loopback development.");
  }
  const profile = assertProfile(options.profile || "monitoring_only");
  const session = await rotateHostedSessionIfNeeded({
    bridgeUrl,
    session: options.session,
    fetchImpl: options.fetchImpl,
    now: options.now,
  });
  const claimed = await deviceRequest({
    bridgeUrl,
    deviceSession: session.token,
    pathname: "/v1/device/jobs/claim",
    body: {},
    fetchImpl: options.fetchImpl,
  });
  if (!claimed.job) return { job: null, session };
  const admitted = admitHostedJob(claimed.job, { deviceId: options.deviceId, profile });
  const heartbeat = await deviceRequest({
    bridgeUrl,
    deviceSession: session.token,
    pathname: `/v1/device/jobs/${encodeURIComponent(admitted.jobId)}/heartbeat`,
    body: { claimToken: claimed.claimToken },
    fetchImpl: options.fetchImpl,
  });
  let claimToken = heartbeat.claimToken;
  const pairing = {
    profile,
    workspaceRoot: path.resolve(options.workspaceRoot),
    homeRoot: path.resolve(options.homeRoot || os.homedir()),
    npmFeedFile: options.npmFeedFile ? path.resolve(options.npmFeedFile) : undefined,
    consents: {},
  };

  try {
    if (admitted.capability.requiresConsent) {
      const consentBroker = options.consentBroker || createNativeConsentBroker();
      await consentBroker({
        job: {
          jobId: admitted.jobId,
          specDigest: admitted.specDigest,
          spec: {
            deviceId: admitted.deviceId,
            capabilityId: admitted.capability.id,
            parameters: admitted.parameters,
            consequences: admitted.capability.consequences,
          },
          consent: { expiresAt: admitted.expiresAt },
        },
      });
      pairing.consents[admitted.capability.id] = true;
    }
    const result = await executeCapability({
      capabilityId: admitted.capability.id,
      parameters: admitted.parameters,
      pairing,
      signal: options.signal,
    });
    const settled = await postHostedReceipt({
      bridgeUrl,
      session,
      jobId: admitted.jobId,
      claimToken,
      outcome: "succeeded",
      receipt: {
        type: "talons.hosted-capability-receipt.v1",
        capabilityId: admitted.capability.id,
        profile,
        result,
      },
      fetchImpl: options.fetchImpl,
    });
    return { job: settled.job, session };
  } catch (error) {
    const denied = /denied|expired without approval/u.test(error instanceof Error ? error.message : "");
    const settled = await postHostedReceipt({
      bridgeUrl,
      session,
      jobId: admitted.jobId,
      claimToken,
      outcome: denied || options.signal?.aborted ? "cancelled" : "failed",
      receipt: {
        type: "talons.hosted-capability-failure.v1",
        capabilityId: admitted.capability.id,
        profile,
        error: safeFailure(error, options),
      },
      fetchImpl: options.fetchImpl,
    });
    return { job: settled.job, session };
  }
}

export function admitHostedJob(job, { deviceId, profile }) {
  if (!job || typeof job !== "object" || job.deviceId !== deviceId || job.profile !== profile
    || typeof job.jobId !== "string" || typeof job.specDigest !== "string"
    || typeof job.operationId !== "string" || !job.parameters || typeof job.parameters !== "object"
    || Array.isArray(job.parameters)) {
    throw new Error("The hosted bridge returned a malformed or cross-device job.");
  }
  const capability = CAPABILITY_REGISTRY[job.operationId];
  if (!capability || !permits(profile, capability) || !capability.allowedProfiles.includes(profile)) {
    throw new Error("The hosted job is not admitted by this local profile and capability registry.");
  }
  if ((job.kind === "assessment" && capability.kind !== "assessment")
    || (job.kind === "remediation" && capability.kind !== "remediation")
    || !["assessment", "remediation"].includes(job.kind)) {
    throw new Error("The hosted job kind does not match the registered local capability.");
  }
  return {
    jobId: job.jobId,
    deviceId: job.deviceId,
    parameters: structuredClone(job.parameters),
    specDigest: job.specDigest,
    expiresAt: job.expiresAt,
    capability,
  };
}

async function postHostedReceipt({ bridgeUrl, session, jobId, claimToken, outcome, receipt, fetchImpl }) {
  return deviceRequest({
    bridgeUrl,
    deviceSession: session.token,
    pathname: `/v1/device/jobs/${encodeURIComponent(jobId)}/receipts`,
    body: { claimToken, outcome, receipt },
    fetchImpl,
  });
}

async function deviceRequest({ bridgeUrl, deviceSession, pathname, body, fetchImpl = fetch }) {
  const response = await fetchImpl(new URL(pathname, bridgeUrl), {
    method: "POST",
    headers: {
      authorization: `DeviceSession ${deviceSession}`,
      "content-type": "application/json",
      accept: "application/json",
    },
    body: JSON.stringify(body),
  });
  let payload;
  try { payload = await response.json(); } catch { throw new Error(`Hosted bridge returned HTTP ${response.status} without JSON.`); }
  if (!response.ok) {
    const error = new Error(`Hosted bridge request failed (${String(payload?.error || response.status)}).`);
    error.code = payload?.error;
    throw error;
  }
  return payload;
}

function safeFailure(error, options) {
  let message = error instanceof Error ? error.message : "Local capability failed.";
  for (const [replacement, value] of [["[workspace]", options.workspaceRoot], ["[home]", options.homeRoot]]) {
    if (value) message = message.replaceAll(path.resolve(value), replacement);
  }
  if (message.length > 500) message = `${message.slice(0, 497)}...`;
  return { code: typeof error?.code === "string" ? error.code : "capability_failed", message };
}

function isLoopbackHttp(url) {
  return url.protocol === "http:" && ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname);
}
