'use strict';

const brain = require('../brain');
const { buildStateSnapshot, isStateEmpty, formatSnapshot } = require('../context/state-snapshot');
const { canActAutonomously, getDomainConfidence } = require('../decision/confidence');
const { getDefaultClient, getDefaultModel, getDefaultProviderType, resolveCompatibleModel } = require('../llm/client');
const { randomUUID: uuidv4 } = require('node:crypto');
const { buildDisciplineHeader } = require('./loop-prompt-discipline');
const { augmentSystemPrompt } = require('./loop-directives');
const telemetry = require('../telemetry');
const { classifyProviderError, recordProviderFailureAlert } = require('../llm/provider-error');

const COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes between same decisions
const PROVIDER_COOLDOWN_KV = 'initiative_provider_cooldown';
const PROVIDER_COOLDOWN_MS = {
  auth_error: 6 * 60 * 60 * 1000,
  quota_exceeded: 6 * 60 * 60 * 1000,
  rate_limited: 2 * 60 * 60 * 1000,
  network: 20 * 60 * 1000,
  provider_unavailable: 20 * 60 * 1000,
  timeout: 20 * 60 * 1000,
};
const PROVIDER_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1000;
const PROVIDER_COOLDOWN_FAILURE_MEMORY_MS = 24 * 60 * 60 * 1000;
const PROVIDER_COOLDOWN_TYPES = new Set(Object.keys(PROVIDER_COOLDOWN_MS));
let providerCooldown = null;

/**
 * Read ~/.walle/HEARTBEAT.md (or opts.heartbeatPath for tests). Returns
 * the readContractFile result; callers branch on the status to decide
 * whether to skip the LLM call this tick.
 */
function _readHeartbeatFile(opts) {
  const path = require('node:path');
  const os = require('node:os');
  const filePath = opts?.heartbeatPath
    || process.env.WALLE_HEARTBEAT_FILE
    || path.join(process.env.WALLE_DATA_DIR || path.join(os.homedir(), '.walle'), 'HEARTBEAT.md');
  const { readContractFile } = require('../utils/heartbeat-content');
  return readContractFile(filePath);
}

/**
 * Build the system prompt for the initiative engine.
 *
 * The first block is loop-tick discipline (borrowed from OpenClaw's
 * heartbeat pattern, see ./loop-prompt-discipline.js): the model is told
 * explicitly that this is NOT a chat continuation and that "noop" is the
 * correct answer most of the time. Empirically the LLM defaults to "be
 * helpful by suggesting something" when the prompt looks chat-shaped, so
 * the explicit framing matters even though the structured response format
 * already enumerates noop.
 */
function buildInitiativePrompt(snapshot, recentDecisions, heartbeatContent = null) {
  const snapshotText = formatSnapshot(snapshot);

  const recentText = recentDecisions.length > 0
    ? recentDecisions.map(d => `- ${d.decision}: ${d.decision_data || '(no data)'}`).join('\n')
    : '(none)';

  const disciplineHeader = buildDisciplineHeader({
    loopName: 'initiative',
    cadence: 'every 5 minutes',
    fastExitToken: 'DECISION: noop',
  });

  // If the user has put guidance in HEARTBEAT.md, surface it to the model
  // — it's a workspace contract that should steer initiative decisions.
  // Borrowed from OpenClaw's HEARTBEAT.md pattern (HEARTBEAT_PROMPT).
  const heartbeatBlock = heartbeatContent
    ? `\nHEARTBEAT.md (workspace contract — follow this strictly):\n${heartbeatContent}\n`
    : '';

  return `${disciplineHeader}You are WALL-E's initiative engine. You observe the owner's current state and decide whether to take action.
${heartbeatBlock}
CURRENT STATE:
${snapshotText}

RECENT DECISIONS (avoid repeating):
${recentText}

PROACTIVITY RULES:
- Default to "noop" -- only act when there's clear value
- Time-sensitive items (calendar in <15min, direct @mention) are highest priority
- Never send the same notification twice within 30 minutes
- Consider: is this ACTUALLY useful right now, or just busy work?
- Calendar items from briefing_items may be stale or recurring — verify the event ACTUALLY occurs today (check the day of week and date) before notifying. Do NOT notify about a "Monday standup" on Sunday.

Respond in this EXACT format:
DECISION: noop|notify|create_task|run_skill
DOMAIN: general|calendar|slack|tasks|knowledge
REASONING: <1-2 sentences explaining why>
DATA: <JSON with details if not noop>`;
}

/**
 * Parse Claude's structured decision output.
 */
function parseDecision(text) {
  const result = {
    decision: 'noop',
    domain: 'general',
    reasoning: '',
    data: null,
  };

  const decisionMatch = text.match(/^DECISION:\s*(\S+)/m);
  if (decisionMatch) {
    const d = decisionMatch[1].toLowerCase().trim();
    if (['noop', 'notify', 'create_task', 'run_skill'].includes(d)) {
      result.decision = d;
    }
  }

  const domainMatch = text.match(/^DOMAIN:\s*(\S+)/m);
  if (domainMatch) {
    result.domain = domainMatch[1].toLowerCase().trim();
  }

  const reasoningMatch = text.match(/^REASONING:\s*(.+)/m);
  if (reasoningMatch) {
    result.reasoning = reasoningMatch[1].trim();
  }

  const dataMatch = text.match(/^DATA:\s*(.+)/ms);
  if (dataMatch) {
    const raw = dataMatch[1].trim();
    if (raw && raw !== 'null' && raw !== 'none' && raw !== 'N/A') {
      try {
        // Try to extract JSON from the data line (may span multiple lines)
        const jsonMatch = raw.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
          result.data = JSON.parse(jsonMatch[0]);
        } else {
          // No JSON object found, store as raw string
          result.data = { raw: raw.slice(0, 500) };
        }
      } catch (_) {
        // If JSON parse fails, store as string
        result.data = { raw: raw.slice(0, 500) };
      }
    }
  }

  return result;
}

/**
 * Get the autonomy tier for a domain.
 */
function getTier(domain) {
  try {
    const dc = getDomainConfidence(domain);
    return dc.current_tier;
  } catch (_) {
    return 1;
  }
}

/**
 * Log an initiative decision to the database.
 */
function logInitiative(trigger, snapshot, reasoning, decision, decisionData, autonomyTier) {
  return brain.insertInitiativeLog({
    trigger,
    state_snapshot: JSON.stringify(snapshot).slice(0, 5000),
    reasoning: reasoning ? reasoning.slice(0, 2000) : null,
    decision,
    decision_data: decisionData ? JSON.stringify(decisionData).slice(0, 5000) : null,
    autonomy_tier: autonomyTier || 1,
  });
}

/**
 * Execute a decision based on its type.
 */
async function executeDecision(decision, snapshot) {
  switch (decision.decision) {
    case 'noop':
      return;

    case 'notify': {
      // Send notification via chat message log (visible in CTM dashboard)
      const msg = decision.data?.message || decision.reasoning || 'Initiative notification';
      brain.insertChatMessage({
        role: 'assistant',
        content: `[Initiative] ${msg}`,
        channel: 'initiative',
        session_id: 'initiative-' + new Date().toISOString().slice(0, 10),
      });
      // Send macOS notification + open CTM WALL-E chat on click
      try {
        const { sendNotification } = require('../tools/local-tools');
        const ctmPort = process.env.CTM_PORT || 3456;
        await sendNotification('WALL-E', msg.slice(0, 200), {
          subtitle: `http://localhost:${ctmPort}/#walle`,
        });
      } catch {}
      return;
    }

    case 'create_task': {
      const taskData = decision.data || {};
      brain.insertTask({
        title: taskData.title || decision.reasoning || 'Auto-created task',
        description: taskData.description || decision.reasoning,
        priority: taskData.priority || 'normal',
        type: 'once',
        due_at: taskData.due_at || null,
      });
      return;
    }

    case 'run_skill': {
      // Attempt to run a skill by name
      const skillName = decision.data?.skill;
      if (!skillName) return;
      try {
        const { executeSkill } = require('../skills/skill-executor');
        const skill = brain.getSkillByName(skillName);
        if (skill && skill.enabled) {
          await executeSkill(skill);
        }
      } catch (err) {
        console.error('[wall-e] Initiative skill execution failed:', err.message);
      }
      return;
    }

    default:
      return;
  }
}

/**
 * Check cooldown: is the same decision type + domain already in recent logs?
 */
function isOnCooldown(decision, recentDecisions) {
  if (decision.decision === 'noop') return false;
  return recentDecisions.some(d => {
    if (d.decision !== decision.decision) return false;
    // For notify, only cooldown if the message is the same
    if (decision.decision === 'notify') {
      try {
        const prev = JSON.parse(d.decision_data);
        return prev?.message && decision.data?.message &&
               prev.message === decision.data.message;
      } catch (_) {
        return true; // Can't parse — assume duplicate
      }
    }
    // For other decision types, same type = on cooldown
    return true;
  });
}

function readProviderCooldownRaw() {
  try {
    const raw = brain.getKv(PROVIDER_COOLDOWN_KV);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return null;
    return parsed;
  } catch {
    return null;
  }
}

function persistProviderCooldown(cooldown) {
  try {
    brain.setKv(PROVIDER_COOLDOWN_KV, cooldown ? JSON.stringify(cooldown) : '');
  } catch {}
}

function clearProviderCooldown() {
  providerCooldown = null;
  persistProviderCooldown(null);
}

function normalizeProviderCooldown(cooldown) {
  if (!cooldown || typeof cooldown !== 'object') return null;
  const until = Number(cooldown.until);
  if (!Number.isFinite(until)) return null;
  return {
    type: cooldown.type || 'provider_error',
    provider: cooldown.provider || null,
    model: cooldown.model || null,
    message: cooldown.message || '',
    until,
    cooldownMs: Number(cooldown.cooldownMs) || 0,
    failures: Math.max(1, Number(cooldown.failures) || 1),
    last_error_at: Number(cooldown.last_error_at) || (until - (Number(cooldown.cooldownMs) || 0)),
  };
}

function getActiveProviderCooldown(now = Date.now()) {
  const cooldown = normalizeProviderCooldown(providerCooldown) || normalizeProviderCooldown(readProviderCooldownRaw());
  if (!cooldown) return null;
  if (cooldown.until <= now) {
    providerCooldown = null;
    return null;
  }
  providerCooldown = cooldown;
  return providerCooldown;
}

function setProviderCooldown(providerError, now = Date.now()) {
  if (!providerError || !PROVIDER_COOLDOWN_TYPES.has(providerError.type)) return null;
  const previous = normalizeProviderCooldown(readProviderCooldownRaw()) || normalizeProviderCooldown(providerCooldown);
  const sameProviderError = previous
    && previous.type === providerError.type
    && (previous.provider || null) === (providerError.provider || null)
    && (previous.model || null) === (providerError.model || null)
    && now - previous.last_error_at <= PROVIDER_COOLDOWN_FAILURE_MEMORY_MS;
  const failures = sameProviderError ? Math.min((previous.failures || 1) + 1, 6) : 1;
  const baseCooldownMs = PROVIDER_COOLDOWN_MS[providerError.type] || 10 * 60 * 1000;
  const cooldownMs = Math.min(baseCooldownMs * Math.pow(2, failures - 1), PROVIDER_COOLDOWN_MAX_MS);
  providerCooldown = {
    type: providerError.type,
    provider: providerError.provider || null,
    model: providerError.model || null,
    message: providerError.userMessage || providerError.message || '',
    until: now + cooldownMs,
    cooldownMs,
    failures,
    last_error_at: now,
  };
  persistProviderCooldown(providerCooldown);
  try {
    telemetry.track('initiative_provider_cooldown', {
      type: providerCooldown.type,
      provider: providerCooldown.provider,
      model: providerCooldown.model,
      cooldown_seconds: Math.round(cooldownMs / 1000),
      failures,
    });
  } catch {}
  return providerCooldown;
}

function skippedProviderCooldown(cooldown, reason = 'provider-cooldown') {
  return {
    skipped: true,
    loop: 'initiative',
    reason,
    provider_error_type: cooldown.type,
    provider: cooldown.provider,
    model: cooldown.model,
    retry_at: new Date(cooldown.until).toISOString(),
  };
}

/**
 * Main initiative loop function. Called on an interval from agent.js
 */
async function runInitiativeLoop(opts = {}) {
  const { shouldRunLoop, skipped } = require('./loop-precheck');
  const now = typeof opts.now === 'function' ? opts.now() : Date.now();
  const activeCooldown = getActiveProviderCooldown(now);
  if (activeCooldown) {
    return skippedProviderCooldown(activeCooldown);
  }

  const pre = shouldRunLoop({ loopName: 'initiative' });
  if (!pre.run) {
    if (pre.reason === 'no-provider' && !runInitiativeLoop._pauseLogged) {
      console.log('[initiative] No AI provider available -- pausing');
      runInitiativeLoop._pauseLogged = true;
    }
    return skipped({ loopName: 'initiative', reason: pre.reason });
  }
  if (runInitiativeLoop._pauseLogged) {
    console.log('[initiative] Provider available -- resuming');
    runInitiativeLoop._pauseLogged = false;
  }

  const snapshot = buildStateSnapshot();

  // Check if there's anything worth reasoning about
  if (isStateEmpty(snapshot)) {
    return { decision: 'noop', reasoning: 'Nothing to evaluate' };
  }

  // HEARTBEAT.md workspace contract — port of OpenClaw's
  // isHeartbeatContentEffectivelyEmpty (src/auto-reply/heartbeat.ts:32-60).
  // If the user has put content in ~/.walle/HEARTBEAT.md, surface it to
  // the model. If the file exists but is just headers/whitespace/empty
  // checkboxes, skip the LLM call entirely — no point spending tokens on
  // "nothing to do" when the user has explicitly drained the work queue.
  const heartbeatContent = _readHeartbeatFile(opts);
  if (heartbeatContent.status === 'effectively-empty' || heartbeatContent.status === 'empty') {
    return {
      decision: 'noop',
      reasoning: 'HEARTBEAT.md is effectively empty — nothing to do',
      heartbeat_status: heartbeatContent.status,
    };
  }

  // Check cooldown -- avoid repeating recent decisions
  const db = brain.getDb();
  const recentDecisions = db.prepare(
    "SELECT decision, decision_data FROM initiative_log WHERE created_at > datetime('now', '-30 minutes') ORDER BY created_at DESC LIMIT 10"
  ).all();

  // Call Claude with a focused prompt
  const provider = opts.client || getDefaultClient();
  const model = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 30000);

  let decision;
  try {
    const response = await provider.chat({
      model,
      maxTokens: 1024,
      system: augmentSystemPrompt(
        'initiative',
        buildInitiativePrompt(
          snapshot,
          recentDecisions,
          heartbeatContent.status === 'ok' ? heartbeatContent.content : null,
        ),
      ),
      messages: [{ role: 'user', content: 'Evaluate current state and decide your next action.' }],
      signal: controller.signal,
    });

    const text = response.content || '';

    decision = parseDecision(text);
    clearProviderCooldown();
  } catch (err) {
    const providerError = classifyProviderError(err, {
      provider: provider.type || getDefaultProviderType(),
      model,
    });
    const cooldown = setProviderCooldown(providerError, now);
    if (cooldown) {
      try { recordProviderFailureAlert(providerError, brain); } catch {}
      try {
        const { default: providerAvailability } = require('../llm/provider-availability');
        const registeredProv = providerAvailability.getConfiguredProviders()
          .find(p => p.providerType === providerError.provider || p.providerId === providerError.provider);
        if (registeredProv) providerAvailability.recordFailure(registeredProv.providerId, providerError.userMessage || err.message);
      } catch {}
      return skippedProviderCooldown(cooldown, 'provider-cooldown-started');
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }

  // Gate by autonomy tier
  const domain = decision.domain || 'general';
  const isHighRisk = decision.decision === 'notify';

  if (decision.decision !== 'noop') {
    // Check cooldown
    if (isOnCooldown(decision, recentDecisions)) {
      logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, 'cooldown', decision.data, getTier(domain));
      return { decision: 'cooldown', reasoning: decision.reasoning };
    }

    // Proactivity model: start conservative, graduate
    if (!canActAutonomously(domain, isHighRisk)) {
      // Below tier threshold -- log observation but don't act
      logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, 'observed', decision.data, getTier(domain));
      return { decision: 'observed', reasoning: decision.reasoning, original: decision.decision };
    }
  }

  // Execute the decision
  if (decision.decision !== 'noop') {
    await executeDecision(decision, snapshot);
  }
  logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, decision.decision, decision.data, getTier(domain));

  return decision;
}

module.exports = {
  runInitiativeLoop,
  parseDecision,
  buildInitiativePrompt,
  isOnCooldown,
  // Exported for testing
  _test: {
    logInitiative,
    executeDecision,
    getTier,
    getActiveProviderCooldown,
    setProviderCooldown,
    resetProviderCooldown: clearProviderCooldown,
    resetProviderCooldownMemoryOnly: () => { providerCooldown = null; },
  },
};
