import { createHash } from "node:crypto";
import { basename, resolve } from "node:path";
import { stewardFallbackTarget } from "./config";
import { getSpawnPolicy, getStewardConfig, setConfig } from "./config-store";
import { listOrchestrators } from "./db";
import { getLifecycleManager } from "./lifecycle-manager";
import { renderTemplate } from "./prompt-resolver";
import { resolveInternalSpawnTarget } from "./auto-spawn-dispatch";
import type { DispatchPin } from "./task-dispatcher";
import { effectiveProviderCatalogList } from "./provider-catalog-store";
import type { Orchestrator, SpawnPolicy, StewardConfig } from "./types";
import { isPathWithinBase } from "./utils";

/** Stable, readable, collision-resistant policy name for a repo's steward. */
export function repoStewardPolicyName(repoRoot: string): string {
  const slug = basename(repoRoot).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
  const hash = createHash("sha1").update(resolve(repoRoot)).digest("hex").slice(0, 8);
  return `steward-${slug}-${hash}`;
}

/**
 * The steward's system prompt — provider-agnostic (works for any provider/model).
 * It tells the agent the workflow via the `agent-relay workspace`/`steward` CLI
 * toolkit (#208): see the queue, CLAIM a workspace so auto-merge yields, inspect
 * its diagnostics, rebase/resolve/check, land green ones, release on escalation.
 */
export function buildStewardPrompt(repoRoot: string): string {
  // #642 — escalation routes to the branch agent's COORDINATOR (named per-workspace in the
  // wake/task message via #640), not a static `system` dead-end. The fallback target below is
  // used ONLY when a wake message names no coordinator.
  const fallback = stewardFallbackTarget();
  return renderTemplate("steward.prompt", {
    repoRoot,
    escalationTarget: fallback ? `\`${fallback}\`` : "the human operator who configured this relay",
  });
}

function desiredStewardPolicy(repoRoot: string, owner: Orchestrator, config: StewardConfig): SpawnPolicy {
  // Launch (and scope the runtime token) at the orchestrator's baseDir, not the
  // repo: isolated worktrees live at `<baseDir>/.agent-relay/workspaces/...`
  // (orchestrator/src/spawn.ts), OUTSIDE repo_root. The steward must reach both the
  // repo and those worktrees, and its provider-agent token's cwdPrefixes derive
  // from this cwd — baseDir is their common ancestor, so the merge action against a
  // worktree path passes authorization. The prompt scopes it to the specific repo.

  // #901 — resolve via the auto-spawn dispatcher so routing policy + quota bands pick
  // the model. StewardConfig.provider/model/effort are OPTIONAL EXPLICIT PINS: when set
  // by the operator they fold into the DispatchPin and win over the router's choice.
  const pin: DispatchPin = {
    provider: config.provider,
    ...(config.model ? { model: config.model } : {}),
    ...(config.effort ? { effort: config.effort } : {}),
  };
  const resolved = resolveInternalSpawnTarget("landing-steward", pin);

  return {
    name: repoStewardPolicyName(repoRoot),
    description: `Autonomous repo steward for ${repoRoot} (issue #167).`,
    enabled: true,
    orchestratorId: owner.id,
    cwd: owner.baseDir,
    provider: resolved.target.provider,
    workspaceMode: "inherit",
    ...(resolved.target.model ? { model: resolved.target.model } : {}),
    ...(resolved.target.effort ? { effort: resolved.target.effort } : {}),
    providerArgs: [],
    prompt: buildStewardPrompt(repoRoot),
    tags: ["steward"],
    capabilities: ["merge", "review"],
    label: `steward:${basename(repoRoot)}`,
    mode: "on-demand",
    permissionMode: config.permissionMode,
    restartOnUpdate: false,
    scheduledDailyRestart: false,
    onDemand: { keepaliveSeconds: config.keepaliveSeconds, idleDefinition: "no-activity" },
    backoff: { schedule: [30, 60, 180], resetAfterSeconds: 300 },
  };
}

// Fields whose change means the persisted policy should be refreshed. Avoids
// churning the config version (and the lifecycle reconcile) every sweep.
//
// Churn guard (#901): model-only diffs are suppressed when the stored model is still
// enabled+available — quota-band oscillation would otherwise change the resolved model on
// every sweep and trigger a lifecycle reconcile each time. Provider changes ARE compared —
// a genuine provider switch must always propagate.
// Self-heal exception: if the stored model is explicitly disabled/unavailable in the
// effective catalog, allow the diff so the steward refreshes onto an enabled model.
function stewardPolicyDiffers(existing: SpawnPolicy, desired: SpawnPolicy): boolean {
  if (
    existing.enabled !== desired.enabled ||
    existing.provider !== desired.provider ||
    existing.effort !== desired.effort ||
    existing.permissionMode !== desired.permissionMode ||
    existing.cwd !== desired.cwd ||
    existing.orchestratorId !== desired.orchestratorId ||
    existing.prompt !== desired.prompt ||
    existing.onDemand?.keepaliveSeconds !== desired.onDemand?.keepaliveSeconds
  ) return true;

  // Model-only diff: suppress (anti-thrash) unless the stored model is disabled/unavailable.
  if (existing.model !== desired.model && existing.model) {
    const catalog = effectiveProviderCatalogList();
    const providerEntry = catalog.find((c) => c.provider === existing.provider);
    const storedModel = providerEntry?.models.find((m) => m.alias === existing.model || m.providerModel === existing.model);
    // Only trigger refresh when the model is EXPLICITLY disabled/unavailable in the catalog.
    // If it's simply absent (incomplete catalog data), suppress to avoid false churn.
    if (storedModel && (storedModel.disabled || storedModel.unavailable)) return true;
  }

  return false;
}

/**
 * Ensure an on-demand steward spawn policy exists for `repoRoot`, built from the
 * global steward config (issue #167). Returns the policy name (durable `policy:`
 * target the caller wakes), or null when stewards are disabled or no orchestrator
 * owns the repo. Idempotent — refreshes the policy only when config-derived fields
 * change, so it can be called on every sweep without churn.
 */
export function ensureRepoSteward(repoRoot: string): string | null {
  const config = getStewardConfig();
  if (!config.enabled) return null;
  const owner = listOrchestrators().find((orch) => isPathWithinBase(repoRoot, orch.baseDir));
  if (!owner) return null;

  const name = repoStewardPolicyName(repoRoot);
  const desired = desiredStewardPolicy(repoRoot, owner, config);
  const existing = getSpawnPolicy(name);
  if (existing && !stewardPolicyDiffers(existing.value, desired)) return name;

  setConfig("spawn-policy", name, desired, "steward-autoprovision");
  getLifecycleManager().onConfigChanged("spawn-policy", name);
  return name;
}
