import { normalizeText } from "./utils.js";
import { compactRepairPlaybook, repairPlaybook } from "./repairPlaybook.js";

const TERMINAL_STATUSES = new Set(["complete", "partial", "failed", "cancelled", "killed", "interrupted"]);

export class FlowWatchdog {
  constructor({ minimax }) {
    this.minimax = minimax;
  }

  async inspect(run, event = {}) {
    run.flowWatchdog ||= {
      enabled: true,
      checks: 0,
      alerts: [],
      repairs: [],
      lastStepCount: 0,
      lastQualified: 0,
      lastOwnerContactsFound: 0
    };
    const state = run.flowWatchdog;
    if (!state.enabled || TERMINAL_STATUSES.has(run.status)) return null;
    const stepCount = run.steps?.length || 0;
    if (stepCount <= state.lastStepCount) return null;

    state.checks += 1;
    state.lastStepCount = stepCount;
    const alert = this.localAlert(run, event);
    if (!alert) return null;

    state.alerts.unshift({ ...alert, at: run.updatedAt || new Date().toISOString() });
    state.alerts = state.alerts.slice(0, 50);

    const repair = await this.repairAdvice(run, alert);
    if (repair) {
      state.repairs.unshift({ ...repair, at: run.updatedAt || new Date().toISOString() });
      state.repairs = state.repairs.slice(0, 50);
      run.agentDecisions ||= [];
      run.agentDecisions.push({
        at: run.updatedAt || new Date().toISOString(),
        type: "flow_watchdog",
        observation: alert.message,
        action: repair.action,
        reason: repair.reason,
        variable: repair.variable,
        tool: repair.tool,
        runbook: repair.runbook,
        patchTemplate: repair.patchTemplate,
        validationCommand: repair.validationCommand,
        patchIdea: repair.patchIdea
      });
    }
    return repair;
  }

  localAlert(run, event) {
    const counters = run.counters || {};
    const phase = event.phase || "";
    const message = normalizeText(event.message || "");
    const ownerAttempts = counters.ownerRabbitHoles || 0;
    const ownerContacts = counters.ownerContactsFound || 0;
    const qualified = run.qualifiedLeads?.length || 0;
    const failures = run.failures || [];

    if (/minimax|provider|non-json|malformed json|timeout|parse error/i.test(message)) {
      return {
        severity: "high",
        code: "provider_repair_needed",
        message: `Provider repair needed during ${phase || "workflow"}: ${message}`,
        signal: message
      };
    }

    if (phase === "classify" && /recorded failure|missing owner|missing dependency|timeout|failed/i.test(message)) {
      return {
        severity: "high",
        code: "failure_seen",
        message: `Failure detected during ${phase}: ${message}`,
        signal: message
      };
    }

    if (ownerAttempts >= 5 && ownerContacts === 0) {
      return {
        severity: "high",
        code: "owner_contact_zero_yield",
        message: `Owner rabbit-hole yield is 0/${ownerAttempts}; strategy needs broader public-profile and article search.`,
        signal: "zero owner contacts"
      };
    }

    if (ownerAttempts >= 10 && ownerContacts / ownerAttempts < 0.08) {
      return {
        severity: "medium",
        code: "owner_contact_low_yield",
        message: `Owner contact yield is ${ownerContacts}/${ownerAttempts}; broaden owner identity evidence and social profile search.`,
        signal: "low owner-contact yield"
      };
    }

    if ((counters.searchAttempts || 0) >= 3 && qualified === 0 && failures.length) {
      return {
        severity: "medium",
        code: "no_qualified_after_failures",
        message: "No qualified owner contacts yet after multiple search attempts and recorded failures.",
        signal: "stalled qualification"
      };
    }

    return null;
  }

  async repairAdvice(run, alert) {
    const fallback = fallbackRepair(alert);
    if (!this.minimax?.configured?.()) return fallback;
    try {
      const result = await this.minimax.chatJson({
        fallback,
        system:
          "You are Hermes' internal flow watcher. Watch the agentic lead workflow and propose one repair action that helps reach the end goal: certified owner-contact leads. " +
          "Use the repair playbook as your training manual. Do not suggest outbound messaging. Do not weaken owner-contact requirements. Change one variable or one strategy area at a time. " +
          "If source-code changes are needed, choose code_patch_plan and describe the smallest safe patch; do not claim the runtime agent already edited code.",
        user: JSON.stringify({
          task: "Propose the next self-repair action for this run.",
          alert,
          repairPlaybook: compactRepairPlaybook(),
          run: {
            status: run.status,
            input: run.input,
            counters: run.counters,
            recentSteps: (run.steps || []).slice(-12),
            recentFailures: (run.failures || []).slice(-8),
            recentDecisions: (run.agentDecisions || []).slice(-8)
          },
          outputSchema: {
            action: "one concrete repair action",
            reason: "why this should help",
            variable: "one repairPlaybook.allowedRepairVariables value",
            tool: "optional codingTools id",
            runbook: "optional diagnosticRunbooks id",
            patchTemplate: "optional patchTemplates id",
            validationCommand: "optional codingTools command to validate the repair",
            patchIdea: "optional patch idea when variable is code_patch_plan"
          }
        })
      });
      return normalizeRepair({
        action: normalizeText(result.action || fallback.action),
        reason: normalizeText(result.reason || fallback.reason),
        variable: normalizeText(result.variable || fallback.variable),
        tool: normalizeText(result.tool || ""),
        runbook: normalizeText(result.runbook || ""),
        patchTemplate: normalizeText(result.patchTemplate || ""),
        validationCommand: normalizeText(result.validationCommand || ""),
        patchIdea: normalizeText(result.patchIdea || ""),
        providerFallback: Boolean(result.providerFallback),
        providerError: result.providerError || ""
      }, fallback);
    } catch {
      return fallback;
    }
  }
}

function fallbackRepair(alert) {
  if (alert.code === "no_worker_interrupted") {
    return {
      action: "Recover the saved checkpoint, run preflight repair if dependencies are missing, then resume the Hermes worker automatically.",
      reason: "The run has durable state but no active worker; the correct repair is checkpoint recovery plus preflight before continuing.",
      variable: "tool_repair",
      tool: "health_probe",
      runbook: "checkpoint_no_worker_drill"
    };
  }
  if (alert.code === "provider_repair_needed") {
    return {
      action: "Run a bounded MiniMax provider probe, keep deterministic fallback active for this decision, and create a provider timeout patch plan only if repeated failures continue.",
      reason: "The provider failure should not stop the lead workflow; Hermes needs telemetry, fallback behavior, and a small validated patch if the adapter is the fault.",
      variable: "provider_repair",
      tool: "node_probe",
      runbook: "provider_timeout_drill",
      patchTemplate: "provider_timeout_patch",
      validationCommand: "npm run check && npm test",
      patchIdea: "If provider failures repeat, patch src/minimax.js to improve timeout/retry/parsing telemetry and add a regression in scripts/agentic_eval.js."
    };
  }
  if (alert.code === "owner_contact_zero_yield" || alert.code === "owner_contact_low_yield") {
    return {
      action: "Broaden the owner rabbit-hole strategy: add exact-business LinkedIn profile searches, owner/founder article searches, and public directory/profile evidence before rejecting.",
      reason: "The current owner-profile search is not producing owner-level contact evidence.",
      variable: "owner_strategy",
      runbook: "owner_yield_zero_drill"
    };
  }
  if (/missing dependency|tool/i.test(alert.signal || "")) {
    return {
      action: "Run the configured tool repair/bootstrap path, then resume from the saved checkpoint.",
      reason: "The failure is tool-related and should be repaired before changing search strategy.",
      variable: "tool_repair",
      tool: "tool_bootstrap"
    };
  }
  return {
    action: "Change one strategy variable and continue from the current checkpoint instead of repeating the same failed path.",
    reason: "The watcher detected stalled progress toward certified owner-contact leads.",
    variable: "owner_strategy"
  };
}

function normalizeRepair(repair, fallback) {
  const allowedVariables = new Set(repairPlaybook.allowedRepairVariables || []);
  const allowedTools = new Set((repairPlaybook.codingTools || []).map((tool) => tool.id));
  const allowedRunbooks = new Set((repairPlaybook.diagnosticRunbooks || []).map((runbook) => runbook.id));
  const allowedPatchTemplates = new Set((repairPlaybook.patchTemplates || []).map((template) => template.id));
  const normalized = {
    ...repair,
    variable: allowedVariables.has(repair.variable) ? repair.variable : fallback.variable,
    tool: repair.tool && allowedTools.has(repair.tool) ? repair.tool : fallback.tool || "",
    runbook: repair.runbook && allowedRunbooks.has(repair.runbook) ? repair.runbook : fallback.runbook || "",
    patchTemplate: repair.patchTemplate && allowedPatchTemplates.has(repair.patchTemplate) ? repair.patchTemplate : fallback.patchTemplate || ""
  };
  if (!normalized.action) normalized.action = fallback.action;
  if (!normalized.reason) normalized.reason = fallback.reason;
  return normalized;
}
