export const DEFAULT_AGENTRUN_NAMESPACE = "agentrun-v01";
export const DEFAULT_AGENTRUN_MANAGER_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";
export const DEFAULT_HWLAB_AGENTRUN_PROJECT_ID = "pikasTech/HWLAB";
export const HWLAB_AGENTRUN_PROVIDER_ID_ENV = "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID";
export const LEGACY_AGENTRUN_PROVIDER_ID_ENV = "AGENTRUN_PROVIDER_ID";
export const DEFAULT_HWLAB_AGENTRUN_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
export const DEFAULT_HWLAB_AGENTRUN_BRANCH = "v0.2";
export const DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE = "deepseek";
export const DEFAULT_UNIDESK_MAIN_SERVER_ENV = "UNIDESK_MAIN_SERVER_IP";
export const DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES = Object.freeze([
  { name: "hwlab-tools", subpath: "tools", target_path: "tools" },
  { name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" }
]);
export const DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS = Object.freeze([
  { name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }
]);

export function createHwlabAgentRunAssemblySummary(options = {}) {
  const env = options.env ?? process.env;
  const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES);
  const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
  const promptAssembly = createPromptAssemblySummary(promptRefs);
  const repoUrl = optionalString(options.repoUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL ?? env.AGENTRUN_REPO_URL) ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL;
  const branch = optionalString(options.branch ?? env.HWLAB_BOOT_REF ?? env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH ?? env.AGENTRUN_BRANCH) ?? DEFAULT_HWLAB_AGENTRUN_BRANCH;
  const commitId = optionalString(options.commitId ?? env.HWLAB_COMMIT_ID ?? env.HWLAB_GIT_SHA ?? env.HWLAB_REVISION);
  const namespace = optionalString(options.namespace ?? env.AGENTRUN_RUNTIME_NAMESPACE ?? env.HWLAB_CODE_AGENT_AGENTRUN_NAMESPACE) ?? DEFAULT_AGENTRUN_NAMESPACE;
  const managerUrl = optionalString(options.managerUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL ?? env.AGENTRUN_MANAGER_URL) ?? DEFAULT_AGENTRUN_MANAGER_URL;
  return {
    status: bundles.length > 0 ? "ready" : "degraded",
    provider: "agentrun-v01",
    namespace,
    managerUrl,
    resourceBundle: {
      kind: "gitbundle",
      repoUrl,
      branch,
      commitId,
      skillsDir: ".agents/skills"
    },
    counts: {
      bundles: bundles.length,
      promptRefs: promptRefs.length
    },
    bundles: bundles.map((item) => ({ ...item })),
    promptRefs: promptRefs.map((item) => ({ ...item })),
    promptAssembly,
    runtimeEnv: {
      assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
      skillsDirs: "HWLAB_CODE_AGENT_SKILLS_DIRS"
    },
    valuesPrinted: false
  };
}

function createPromptAssemblySummary(promptRefs) {
  const refs = promptRefs.map((item) => ({
    name: item.name,
    path: item.path,
    inject: item.inject,
    stage: promptInjectStageLabel(item.inject),
    required: item.required !== false,
    source: "ResourceBundleRef.promptRefs"
  }));
  return {
    status: refs.length > 0 ? "ready" : "missing",
    count: refs.length,
    requiredCount: refs.filter((item) => item.required).length,
    injectStages: [...new Set(refs.map((item) => item.inject))],
    refs,
    valuesPrinted: false
  };
}

function promptInjectStageLabel(inject) {
  if (inject === "thread-start") return "thread-start initial prompt";
  return inject;
}

export const AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES = Object.freeze([
  "AUTH_PASSWORD",
  "CODEX_API_KEY",
  "GH_TOKEN",
  "GITHUB_TOKEN",
  "OPENAI_API_KEY",
  "PROVIDER_TOKEN",
  "UNIDESK_AUTH_PASSWORD",
  "UNIDESK_PROVIDER_TOKEN",
  "UNIDESK_SSH_CLIENT_TOKEN"
]);

const backendProfileAliases = Object.freeze({ "codex-api": "codex", codex: "codex" });
const backendProfileIdPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u;
const providerSecretKeys = Object.freeze(["auth.json", "config.toml"]);
const reusableCredentialEnvNameSet = new Set(AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES);

export function createGithubToolCredential({
  namespace = DEFAULT_AGENTRUN_NAMESPACE,
  secretName = "agentrun-v01-tool-github-pr",
  secretKey = "GH_TOKEN"
} = {}) {
  return {
    tool: "github",
    purpose: "pull-request",
    secretRef: { namespace, name: secretName, keys: [secretKey] },
    projection: { kind: "env", envName: secretKey, secretKey }
  };
}

export function createUnideskSshToolCredential({
  namespace = DEFAULT_AGENTRUN_NAMESPACE,
  secretName = "agentrun-v01-tool-unidesk-ssh"
} = {}) {
  return {
    tool: "unidesk-ssh",
    purpose: "ssh-passthrough",
    secretRef: { namespace, name: secretName, keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
    projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" }
  };
}

export function createProviderCredential({
  backendProfile = DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE,
  namespace = DEFAULT_AGENTRUN_NAMESPACE,
  secretName = null,
  mountPath = "~/.codex"
} = {}) {
  const profile = normalizeBackendProfile(backendProfile);
  return {
    profile,
    secretRef: {
      namespace,
      name: secretName || `agentrun-v01-provider-${profile}`,
      keys: providerSecretKeys.slice(),
      mountPath
    }
  };
}

export function createHwlabAgentRunDispatchAssembly(options = {}) {
  const env = options.env ?? process.env;
  const traceId = nonEmptyString(options.traceId, "traceId");
  const prompt = nonEmptyString(options.prompt, "prompt");
  const commitId = fullCommitSha(options.commitId);
  // Concept boundary (#792): HWLAB adapter fixes AgentRun policy fields regardless of caller.
  // Business-side projectId belongs in metadata/workspaceRef, NOT AgentRun projectId.
  const backendProfile = normalizeBackendProfile(DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE);
  const includeGithubToolCredential = options.includeGithubToolCredential !== false;
  const includeUnideskSshToolCredential = options.includeUnideskSshToolCredential !== false;
  const namespace = nonEmptyString(options.namespace ?? DEFAULT_AGENTRUN_NAMESPACE, "namespace");
  const managerUrl = trimTrailingSlash(nonEmptyString(options.managerUrl ?? DEFAULT_AGENTRUN_MANAGER_URL, "managerUrl"));
  const projectId = DEFAULT_HWLAB_AGENTRUN_PROJECT_ID;
  const providerId = resolveHwlabAgentRunProviderId(env);
  const repoUrl = nonEmptyString(options.repoUrl ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL, "repoUrl");
  const branch = nonEmptyString(options.branch ?? DEFAULT_HWLAB_AGENTRUN_BRANCH, "branch");
  const timeoutMs = positiveInteger(options.timeoutMs ?? 600000, "timeoutMs");
  const unideskMainServerIp = optionalString(options.unideskMainServerIp ?? env.UNIDESK_MAIN_SERVER_IP ?? env.UNIDESK_MAIN_SERVER_HOST);
  const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES);
  const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
  if (includeUnideskSshToolCredential && !unideskMainServerIp) {
    throw new Error("unideskMainServerIp is required when UniDesk SSH passthrough is enabled");
  }

  const toolCredentials = [
    ...(includeGithubToolCredential ? [createGithubToolCredential({ namespace })] : []),
    ...(includeUnideskSshToolCredential ? [createUnideskSshToolCredential({ namespace })] : []),
    ...normalizeToolCredentials(options.toolCredentials ?? [])
  ];
  const transientEnv = normalizeTransientEnv(transientEnvWithUnideskMainServer(options.transientEnv ?? [], unideskMainServerIp));
  const sessionRef = createSessionRef(options);
  const resourceBundleRef = {
    kind: "gitbundle",
    repoUrl,
    commitId,
    bundles,
    ...(promptRefs.length > 0 ? { promptRefs } : {}),
    ...(options.resourceCredentialRef ? { credentialRef: options.resourceCredentialRef } : {})
  };

  const runPayload = {
    tenantId: "hwlab",
    projectId,
    workspaceRef: {
      kind: "git-worktree",
      repo: repoUrl,
      branch
    },
    ...(sessionRef ? { sessionRef } : { sessionRef: null }),
    resourceBundleRef,
    providerId,
    backendProfile,
    executionPolicy: {
      sandbox: options.sandbox ?? "workspace-write",
      approval: options.approval ?? "never",
      timeoutMs,
      network: options.network ?? "default",
      secretScope: {
        allowCredentialEcho: false,
        providerCredentials: [createProviderCredential({ backendProfile, namespace })],
        ...(toolCredentials.length > 0 ? { toolCredentials } : {})
      }
    },
    traceSink: options.traceSink ?? null
  };

  const commandPayload = {
    type: "turn",
    payload: {
      prompt,
      traceId,
      projectId: DEFAULT_HWLAB_AGENTRUN_PROJECT_ID,
      ...(options.conversationId ? { conversationId: nonEmptyString(options.conversationId, "conversationId") } : {}),
      ...(options.sessionId ? { sessionId: nonEmptyString(options.sessionId, "sessionId") } : {}),
      ...(options.threadId ? { threadId: nonEmptyString(options.threadId, "threadId") } : {}),
      providerProfile: backendProfile,
      dispatchSource: "hwlab-code-agent"
    },
    idempotencyKey: options.commandIdempotencyKey ?? `hwlab:${traceId}:turn`
  };

  const runnerJobPayload = {
    idempotencyKey: options.runnerJobIdempotencyKey ?? `hwlab:${traceId}:runner-job`,
    ...(options.attemptId ? { attemptId: nonEmptyString(options.attemptId, "attemptId") } : {}),
    transientEnv
  };

  return {
    managerUrl,
    runPayload,
    commandPayload,
    runnerJobPayload,
    boundaries: {
      valuesPrinted: false,
      githubCredentialSource: includeGithubToolCredential ? "toolCredentials" : null,
      unideskSshCredentialSource: includeUnideskSshToolCredential ? "toolCredentials" : null,
      unideskMainServerSource: unideskMainServerIp ? "transientEnv" : null,
      bundles: bundles.map((item) => item.name),
      promptRefs: promptRefs.map((item) => item.name),
      transientEnvNames: transientEnv.map((item) => item.name),
      forbiddenTransientEnvNames: AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES.slice()
    }
  };
}

export async function dispatchHwlabAgentRun(options = {}) {
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
  if (typeof fetchImpl !== "function") throw new Error("dispatchHwlabAgentRun requires fetchImpl or global fetch");
  const assembly = options.assembly ?? createHwlabAgentRunDispatchAssembly(options);
  const managerUrl = trimTrailingSlash(options.managerUrl ?? assembly.managerUrl);
  const run = await postJson(fetchImpl, managerUrl, "/api/v1/runs", assembly.runPayload);
  const runId = nonEmptyString(run.id, "run.id");
  const command = await postJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/commands`, assembly.commandPayload);
  const commandId = nonEmptyString(command.id, "command.id");
  const runnerJobPayload = { ...assembly.runnerJobPayload, commandId };
  const runnerJob = await postJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, runnerJobPayload);
  return {
    managerUrl,
    run,
    command,
    runnerJob,
    requests: {
      runPayload: assembly.runPayload,
      commandPayload: assembly.commandPayload,
      runnerJobPayload
    },
    boundaries: assembly.boundaries
  };
}

function createSessionRef(options) {
  const sessionId = optionalString(options.sessionId);
  const conversationId = optionalString(options.conversationId);
  const threadId = optionalString(options.threadId);
  if (!sessionId && !conversationId && !threadId) return null;
  return {
    sessionId: sessionId || conversationId || threadId,
    ...(conversationId ? { conversationId } : {}),
    ...(threadId ? { threadId } : {}),
    metadata: {
      source: "hwlab-code-agent",
      traceId: optionalString(options.traceId) ?? null
    }
  };
}

function normalizeTransientEnv(items) {
  const seen = new Set();
  return items.filter(Boolean).map((item, index) => {
    const name = nonEmptyString(item.name, `transientEnv[${index}].name`);
    if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(name)) throw new Error(`transientEnv[${index}].name must be an uppercase env name`);
    if (reusableCredentialEnvNameSet.has(name)) throw new Error(`transientEnv ${name} must use AgentRun tool/provider credential assembly instead`);
    if (seen.has(name)) throw new Error(`transientEnv ${name} is duplicated`);
    seen.add(name);
    return {
      name,
      value: nonEmptyString(item.value, `transientEnv[${index}].value`),
      sensitive: item.sensitive !== false
    };
  });
}

function transientEnvWithUnideskMainServer(items, unideskMainServerIp) {
  if (!unideskMainServerIp) return items;
  if (items.some((item) => item?.name === DEFAULT_UNIDESK_MAIN_SERVER_ENV)) return items;
  return [...items, { name: DEFAULT_UNIDESK_MAIN_SERVER_ENV, value: unideskMainServerIp, sensitive: false }];
}

function normalizeToolCredentials(items) {
  return items.map((item, index) => {
    if (!item || typeof item !== "object") throw new Error(`toolCredentials[${index}] must be an object`);
    return item;
  });
}

function normalizeResourceGitBundles(items) {
  if (!Array.isArray(items)) throw new Error("bundles must be an array");
  const seen = new Set();
  return items.map((item, index) => {
    if (!item || typeof item !== "object") throw new Error(`bundles[${index}] must be an object`);
    const name = resourceName(item.name, `bundles[${index}].name`);
    if (seen.has(name)) throw new Error(`bundles name ${name} is duplicated`);
    seen.add(name);
    return {
      name,
      subpath: resourcePath(item.subpath, `bundles[${index}].subpath`),
      target_path: workspaceTargetPath(item.target_path ?? item.targetPath, `bundles[${index}].target_path`)
    };
  });
}

function normalizeResourcePromptRefs(items) {
  if (!Array.isArray(items)) throw new Error("promptRefs must be an array");
  const seen = new Set();
  return items.map((item, index) => {
    if (!item || typeof item !== "object") throw new Error(`promptRefs[${index}] must be an object`);
    const name = resourceName(item.name, `promptRefs[${index}].name`);
    if (seen.has(name)) throw new Error(`promptRefs name ${name} is duplicated`);
    seen.add(name);
    const refPath = resourcePath(item.path, `promptRefs[${index}].path`);
    const inject = optionalString(item.inject) ?? "thread-start";
    if (inject !== "thread-start") throw new Error(`promptRefs[${index}].inject must be thread-start`);
    return { name, path: refPath, inject: "thread-start", required: item.required !== false };
  });
}

function resourceName(value, fieldName) {
  const name = nonEmptyString(value, fieldName);
  if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new Error(`${fieldName} must be a lowercase resource name`);
  return name;
}

function resourcePath(value, fieldName) {
  const refPath = nonEmptyString(value, fieldName);
  if (refPath.startsWith("/") || refPath.includes("..")) throw new Error(`${fieldName} must stay within the checkout`);
  return refPath;
}

function workspaceTargetPath(value, fieldName) {
  const targetPath = nonEmptyString(value, fieldName);
  if (targetPath === "." || targetPath.startsWith("/") || targetPath.includes("..")) throw new Error(`${fieldName} must stay within the workspace`);
  return targetPath;
}

async function postJson(fetchImpl, managerUrl, path, payload) {
  const response = await fetchImpl(`${managerUrl}${path}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload)
  });
  const text = await response.text();
  const body = text ? JSON.parse(text) : null;
  if (!response.ok) throw new Error(`AgentRun POST ${path} failed: status=${response.status} body=${JSON.stringify(body).slice(0, 500)}`);
  return unwrapAgentRunEnvelope(path, body);
}

function unwrapAgentRunEnvelope(path, body) {
  if (!body || typeof body !== "object" || !("ok" in body)) return body;
  if (body.ok === true) return body.data ?? null;
  throw new Error(`AgentRun POST ${path} failed: envelope=${JSON.stringify(body).slice(0, 500)}`);
}

function normalizeBackendProfile(value) {
  const profile = nonEmptyString(value, "backendProfile").toLowerCase();
  if (profile === "runtime-default") throw new Error(`unsupported AgentRun backendProfile ${JSON.stringify(profile)}`);
  const normalized = backendProfileAliases[profile] || profile;
  if (!backendProfileIdPattern.test(normalized)) throw new Error(`unsupported AgentRun backendProfile ${JSON.stringify(profile)}`);
  return normalized;
}

function fullCommitSha(value) {
  const commitId = nonEmptyString(value, "commitId");
  if (!/^[0-9a-f]{40}$/u.test(commitId)) throw new Error("commitId must be a full 40-character lowercase git commit sha");
  return commitId;
}

function positiveInteger(value, fieldName) {
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${fieldName} must be a positive integer`);
  return parsed;
}

function resolveHwlabAgentRunProviderId(env = process.env) {
  const providerId = optionalString(env[HWLAB_AGENTRUN_PROVIDER_ID_ENV] ?? env[LEGACY_AGENTRUN_PROVIDER_ID_ENV]);
  if (!providerId) throw new Error(`${HWLAB_AGENTRUN_PROVIDER_ID_ENV} is required and must be injected from the runtime lane node id`);
  if (!/^[A-Za-z0-9._-]+$/u.test(providerId)) throw new Error(`${HWLAB_AGENTRUN_PROVIDER_ID_ENV} must be a simple node id`);
  return providerId;
}

function nonEmptyString(value, fieldName) {
  const text = optionalString(value);
  if (!text) throw new Error(`${fieldName} is required`);
  return text;
}

function optionalString(value) {
  return typeof value === "string" && value.trim() ? value.trim() : null;
}

function trimTrailingSlash(value) {
  return nonEmptyString(value, "url").replace(/\/+$/u, "");
}
