/**
 * Deep tick substeps — individually-exported async functions.
 * Each function wraps its logic in try/catch with ctx.logger.warn() on failure.
 */

import type { GardenerContext } from './gardener-context.js';
import { DEFAULT_USER_ID } from './gardener-context.js';
import { storeFusedMemory } from './gardener-fusion-storage.js';
import { scheduleProactiveItem, getLastProactiveAt, createProactiveItem } from './gardener-scheduling.js';
import { findFusionClusters, fuseMemoryCluster } from './fusion.js';
import { auditRetrievalHistory } from './retrieval-audit.js';
import { archiveLowUtilityMemories, pruneOrphanedRelations } from './utility-score.js';
import { computeTrustScore } from './trust-score.js';
import { checkGoalDeadlines } from './goal-deadline-check.js';
import { evaluateProactive } from './proactive-evaluator.js';
import { getTodayStartMs } from '../proactive/proactive-utils.js';
import { DIAL_BUDGETS, PROACTIVE_COOLDOWN_MS } from '../proactive/proactive-config.js';

// ============ Step functions ============

/**
 * B1: Full decay scan (all memories) + utility-based archival.
 * Combines decay pass with low-utility archival into a single forgetting step.
 */
export function runFullDecay(ctx: GardenerContext): { updated: number; archived: number } {
  try {
    const result = ctx.scallopStore.processFullDecay();

    // Utility-based archival (previously substep B3b)
    let utilityArchived = 0;
    if (!ctx.disableArchival) {
      try {
        const archiveResult = archiveLowUtilityMemories(ctx.db, {
          utilityThreshold: 0.1,
          minAgeDays: 14,
          maxPerRun: 50,
        });
        utilityArchived = archiveResult.archived;
      } catch (err) {
        ctx.logger.warn({ error: (err as Error).message }, 'Utility-based archival failed');
      }
    }

    const totalArchived = result.archived + utilityArchived;
    if (result.updated > 0 || utilityArchived > 0) {
      ctx.logger.info({ updated: result.updated, archived: totalArchived }, 'Full decay processed');
    }
    return { updated: result.updated, archived: totalArchived };
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Full decay failed');
    return { updated: 0, archived: 0 };
  }
}

/**
 * B1.5: Memory fusion (merge dormant related memory clusters)
 */
export async function runMemoryFusion(ctx: GardenerContext): Promise<{ totalFused: number; totalMerged: number }> {
  if (!ctx.fusionProvider) return { totalFused: 0, totalMerged: 0 };

  try {
    const userId = DEFAULT_USER_ID;
    let totalFused = 0;
    let totalMerged = 0;

    const allMemories = ctx.db.getMemoriesByUser(userId, {
      minProminence: 0.1,
      isLatest: true,
      includeAllSources: true,
    });
    const dormantMemories = allMemories.filter(m =>
      m.prominence < 0.7 &&
      m.memoryType !== 'static_profile' &&
      m.memoryType !== 'derived'
    );

    if (dormantMemories.length >= 2) {
      const clusters = findFusionClusters(
        dormantMemories,
        (id) => ctx.db.getRelations(id),
        { minClusterSize: 2, maxClusters: 5, maxProminence: 0.7 },
      );

      for (const cluster of clusters) {
        const result = await fuseMemoryCluster(cluster, ctx.fusionProvider);
        if (!result) continue;

        await storeFusedMemory({
          scallopStore: ctx.scallopStore,
          db: ctx.db,
          userId,
          summary: result.summary,
          category: result.category,
          importance: result.importance,
          confidence: result.confidence,
          sourceMemoryIds: cluster.map(m => m.id),
          sourceChunk: cluster.map(m => m.content).join(' | '),
          learnedFrom: 'consolidation',
          supersedeSources: true,
        }, allMemories);

        totalFused++;
        totalMerged += cluster.length;
      }
    }

    if (totalFused > 0) {
      ctx.logger.info({ fused: totalFused, memoriesMerged: totalMerged }, 'Memory fusion complete');
    }
    return { totalFused, totalMerged };
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Memory fusion failed');
    return { totalFused: 0, totalMerged: 0 };
  }
}

/**
 * B2: Generate session summaries for old sessions before pruning
 */
export async function runSessionSummarization(ctx: GardenerContext): Promise<{ summarized: number }> {
  if (!ctx.sessionSummarizer) return { summarized: 0 };

  try {
    // Summarize sessions idle for 2+ hours (not 24h) so inner thoughts
    // gets input sooner and proactive follow-ups can be generated.
    const cutoffMs = 2 * 60 * 60 * 1000; // 2 hours
    const cutoff = Date.now() - cutoffMs;
    const oldSessions = ctx.db.raw<{ id: string; metadata: string | null }>(
      `SELECT s.id, s.metadata FROM sessions s
       WHERE s.updated_at < ?
         AND NOT EXISTS (SELECT 1 FROM session_summaries ss WHERE ss.session_id = s.id)
       ORDER BY s.updated_at DESC
       LIMIT 20`,
      [cutoff]
    ).filter(s => {
      // Skip sub-agent sessions — transient, results already in parent
      if (s.metadata) {
        try {
          const meta = JSON.parse(s.metadata);
          if (meta.isSubAgent) return false;
        } catch { /* ignore */ }
      }
      return true;
    });
    if (oldSessions.length > 0) {
      const resolvedUserId = DEFAULT_USER_ID;
      const summarized = await ctx.sessionSummarizer.summarizeBatch(
        ctx.db,
        oldSessions.map(s => s.id),
        resolvedUserId
      );
      if (summarized > 0) {
        ctx.logger.info({ summarized, total: oldSessions.length }, 'Session summaries generated');
      }
      return { summarized };
    }
    return { summarized: 0 };
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Session summarization failed');
    return { summarized: 0 };
  }
}

/**
 * B3: Enhanced forgetting pipeline (audit + prune + orphan cleanup).
 * Utility-based archival (formerly 3b) is now handled by runFullDecay (B1).
 */
export async function runEnhancedForgetting(ctx: GardenerContext): Promise<void> {
  let auditNeverRetrieved = 0;
  let auditStaleRetrieved = 0;
  let auditCandidateCount = 0;
  let sessionsDeleted = 0;
  let memoriesDeleted = 0;
  let orphansDeleted = 0;

  // 3a. Retrieval audit — apply small prominence penalty to never/stale-retrieved active memories
  try {
    const auditResult = auditRetrievalHistory(ctx.db);
    auditNeverRetrieved = auditResult.neverRetrieved;
    auditStaleRetrieved = auditResult.staleRetrieved;
    auditCandidateCount = auditResult.candidatesForDecay.length;

    // Demote audit candidates: 5% prominence reduction per deep tick.
    // This nudges never/stale-retrieved active memories toward dormancy
    // so the system gradually forgets memories it never actually uses.
    if (auditResult.candidatesForDecay.length > 0) {
      const demotions: Array<{ id: string; prominence: number }> = [];
      for (const id of auditResult.candidatesForDecay) {
        const mem = ctx.db.getMemory(id);
        if (mem && mem.prominence > 0.1) {
          demotions.push({ id, prominence: mem.prominence * 0.95 });
        }
      }
      if (demotions.length > 0) {
        ctx.db.updateProminences(demotions);
        ctx.logger.debug({ demoted: demotions.length }, 'Audit candidates demoted');
      }
    }
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Retrieval audit failed');
  }

  // 3c. Hard prune truly dead memories
  if (!ctx.disableArchival) {
    try {
      sessionsDeleted = ctx.db.pruneOldSessions(30);
      memoriesDeleted = ctx.db.pruneArchivedMemories(0.01);
    } catch (err) {
      ctx.logger.warn({ error: (err as Error).message }, 'Hard pruning failed');
    }
  }

  // 3d. Prune orphaned relation edges
  try {
    orphansDeleted = pruneOrphanedRelations(ctx.db);
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Orphan relation pruning failed');
  }

  ctx.logger.info(
    {
      auditNeverRetrieved,
      auditStaleRetrieved,
      auditCandidateCount,
      memoriesDeleted,
      sessionsDeleted,
      orphansDeleted,
    },
    'Enhanced forgetting complete'
  );
}

/**
 * B4: Behavioral pattern inference
 */
export function runBehavioralInference(ctx: GardenerContext): { messageCount: number } {
  try {
    const profileManager = ctx.scallopStore.getProfileManager();
    const userId = DEFAULT_USER_ID;
    let totalMessages = 0;

    const recentSessions = ctx.db.listSessions(5);
    const allMessages: Array<{ content: string; timestamp: number }> = [];
    for (const session of recentSessions) {
      const messages = ctx.db.getSessionMessages(session.id);
      for (const msg of messages) {
        if (msg.role === 'user') {
          allMessages.push({ content: msg.content, timestamp: msg.createdAt });
        }
      }
    }
    if (allMessages.length > 0) {
      const sessionSummaries = ctx.db.getSessionSummariesByUser(userId, 20);
      const sessions = sessionSummaries
        .filter(s => s.messageCount > 0)
        .map(s => ({
          messageCount: s.messageCount,
          durationMs: s.durationMs,
          startTime: s.createdAt,
        }));

      const userMemories = ctx.db.getMemoriesByUser(userId, { isLatest: true, limit: 100 });
      const messageEmbeddings = userMemories
        .filter(m => m.embedding != null)
        .map(m => ({
          content: m.content,
          embedding: m.embedding!,
        }));

      profileManager.inferBehavioralPatterns(userId, allMessages, {
        sessions: sessions.length > 0 ? sessions : undefined,
        messageEmbeddings: messageEmbeddings.length > 0 ? messageEmbeddings : undefined,
      });
      ctx.logger.debug({ userId, messageCount: allMessages.length }, 'Behavioral patterns updated');
      totalMessages += allMessages.length;
    }
    return { messageCount: totalMessages };
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Behavioral inference failed');
    return { messageCount: 0 };
  }
}

/**
 * B5: Trust score update
 */
export function runTrustScoreUpdate(ctx: GardenerContext): void {
  try {
    const profileManager = ctx.scallopStore.getProfileManager();
    const userId = DEFAULT_USER_ID;

    const sessionSummaries = ctx.db.getSessionSummariesByUser(userId, 30);
    const sessions = sessionSummaries
      .filter(s => s.messageCount > 0)
      .map(s => ({ messageCount: s.messageCount, durationMs: s.durationMs, startTime: s.createdAt }));

    const rawScheduledItems = ctx.db.getScheduledItemsByUser(userId);
    const scheduledItems = rawScheduledItems
      .filter(i => i.status !== 'expired')
      .map(i => ({ status: i.status as 'pending' | 'fired' | 'acted' | 'dismissed', source: i.source, firedAt: i.firedAt ?? undefined }));
    const existingPatterns = profileManager.getBehavioralPatterns(userId);
    const existingTrust = existingPatterns?.responsePreferences?.trustScore as number | undefined;
    const trustResult = computeTrustScore(sessions, scheduledItems, { existingScore: existingTrust });
    if (trustResult) {
      profileManager.updateBehavioralPatterns(userId, {
        responsePreferences: {
          ...(existingPatterns?.responsePreferences ?? {}),
          trustScore: trustResult.trustScore,
          proactivenessDial: trustResult.proactivenessDial,
        },
      });
      ctx.logger.debug({ userId, trustScore: trustResult.trustScore, dial: trustResult.proactivenessDial }, 'Trust score updated');
    }
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Trust score update failed');
  }
}

/**
 * B6: Goal deadline check
 */
export async function runGoalDeadlineCheck(ctx: GardenerContext): Promise<void> {
  try {
    const GoalService = (await import('../goals/goal-service.js')).GoalService;
    const goalService = new GoalService({ db: ctx.db, logger: ctx.logger });
    const userId = DEFAULT_USER_ID;

    const activeGoals = await goalService.listGoals(userId, { status: 'active' });
    const goalsWithDueDates = activeGoals.filter(g => g.metadata.dueDate != null);
    if (goalsWithDueDates.length > 0) {
      const pendingItems = ctx.db.getDueScheduledItems(Date.now() + 30 * 24 * 60 * 60 * 1000);
      const existingReminders = pendingItems.map(item => ({ message: item.message }));
      const deadlineResult = checkGoalDeadlines(goalsWithDueDates, existingReminders);
      for (const notification of deadlineResult.notifications) {
        createProactiveItem({
          db: ctx.db,
          userId: notification.userId,
          message: notification.message,
          context: null,
          type: 'goal_checkin',
          kind: 'nudge',
          quietHours: ctx.quietHours,
          activeHours: [],
          lastProactiveAt: null,
          urgency: 'high',
          sourceMemoryId: notification.goalId,
          timezone: ctx.getTimezone?.(notification.userId),
        });
      }
      if (deadlineResult.approaching.length > 0) {
        ctx.logger.info({ userId, approaching: deadlineResult.approaching.length, notifications: deadlineResult.notifications.length }, 'Goal deadline check complete');
      }
    }
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Goal deadline check failed');
  }
}

/**
 * B7: Unified proactive evaluation (session context + system gaps in one LLM call).
 * Replaces the previous two-path approach (inner-thoughts + gap-scanner).
 */
export async function runInnerThoughts(ctx: GardenerContext): Promise<void> {
  if (!ctx.fusionProvider) return;

  try {
    const userId = DEFAULT_USER_ID;
    // Widened from 6h to 48h — between active chat windows the evaluator was
    // seeing zero session context, which left it with only low-severity
    // anomaly signals and the LLM always returned empty items.
    const SESSION_CONTEXT_WINDOW_MS = 48 * 60 * 60 * 1000;
    const contextCutoff = Date.now() - SESSION_CONTEXT_WINDOW_MS;

    const allSummaries = ctx.db.getSessionSummariesByUser(userId, 20);
    const recentSummaries = allSummaries.filter(s => s.createdAt >= contextCutoff);
    // Fall back to the most recent summary of any age if the 48h window has
    // nothing — better stale context than no context when deciding to check in.
    const sessionSummary = recentSummaries.length > 0
      ? recentSummaries[0]
      : (allSummaries.length > 0 ? allSummaries[0] : null);

    const behavioralPatterns = ctx.db.getBehavioralPatterns(userId);
    const affect = behavioralPatterns?.smoothedAffect ?? null;
    const storedDial = (behavioralPatterns?.responsePreferences?.proactivenessDial as 'conservative' | 'moderate' | 'eager') ?? 'moderate';
    const activeHours = behavioralPatterns?.activeHours ?? [];
    const lastProactiveAt = getLastProactiveAt(ctx.db, userId);

    // Load goals and board items for gap scanning
    const GoalService = (await import('../goals/goal-service.js')).GoalService;
    const goalService = new GoalService({ db: ctx.db, logger: ctx.logger });
    const activeGoals = await goalService.listGoals(userId, { status: 'active' });

    const existingItems = ctx.db.getScheduledItemsByUser(userId);
    const boardItems = existingItems
      .filter(i => i.boardStatus === 'in_progress' || i.boardStatus === 'waiting')
      .map(i => ({
        id: i.id,
        title: i.message,
        boardStatus: i.boardStatus!,
        updatedAt: i.updatedAt,
        priority: i.priority,
      }));

    // Count today's items for budget
    const tz = ctx.getTimezone?.(userId) ?? 'UTC';
    const todayStart = getTodayStartMs(tz);
    const todayItemCount = existingItems.filter(
      i => i.source === 'agent' && i.createdAt >= todayStart
    ).length;

    const pendingItems = existingItems
      .filter(i => i.status === 'pending' || i.status === 'fired')
      .map(i => ({ message: i.message, context: i.context }));

    // Pull user-stated preferences relevant to proactive behavior so the
    // evaluator prompt can honor them. Keyword filter is intentional — we only
    // care about preferences that tell the bot how often to check in, not
    // generic lifestyle facts.
    const PROACTIVE_PREF_RE = /\b(proactive|proactively|check in|check-in|remind|follow[- ]?up|ping me|nudge|initiate|reach out)\b/i;
    const userPreferences = ctx.db
      .getMemoriesByUser(userId, { limit: 100 })
      .filter(m => PROACTIVE_PREF_RE.test(m.content))
      .slice(0, 5)
      .map(m => m.content);

    // Elevate dial to 'eager' when the user has explicitly asked for more
    // proactive check-ins — overrides whatever got auto-set in behavioral
    // patterns. Without this, moderate's "skip low-severity" rule dominates
    // even though the user's stated preference says otherwise.
    const dial = (userPreferences.length > 0 && storedDial !== 'eager')
      ? 'eager'
      : storedDial;

    const result = await evaluateProactive({
      sessionSummary,
      behavioralPatterns,
      activeGoals,
      boardItems,
      allSessionSummaries: allSummaries,
      existingItems: pendingItems,
      dial,
      affect,
      lastProactiveAt,
      activeHours,
      userId,
      todayItemCount,
      userPreferences,
    }, ctx.fusionProvider);

    // Schedule each output item
    for (const item of result.items) {
      const timingUrgency: 'low' | 'medium' | 'high' =
        item.severity === 'high' ? 'high' :
        item.severity === 'medium' ? 'medium' : 'low';

      const proResult = createProactiveItem({
        db: ctx.db,
        userId,
        message: item.message,
        context: item.context,
        type: 'follow_up',
        kind: 'nudge',
        quietHours: ctx.quietHours,
        activeHours,
        lastProactiveAt,
        urgency: timingUrgency,
        timezone: ctx.getTimezone?.(userId),
      });

      if (proResult.created) {
        ctx.logger.info({ userId, urgency: timingUrgency, gapType: item.gapType }, 'Proactive evaluator created item');
      } else {
        // Item generated but a near-identical one was already pending.
        ctx.db.recordProactiveDecision({
          userId,
          stage: 'create',
          outcome: 'deduped',
          reason: 'pre_create_dedup',
          detail: { gapType: item.gapType, message: item.message.slice(0, 120) },
        });
      }
    }

    if (result.items.length > 0 || result.signalsFound > 0) {
      ctx.logger.info({
        userId,
        signalsFound: result.signalsFound,
        itemsCreated: result.items.length,
        llmCalled: result.llmCalled,
        skipReason: result.skipReason,
        ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
        ...(result.unparsedResponseLength !== undefined
          ? { unparsedResponseLength: result.unparsedResponseLength }
          : {}),
      }, 'Proactive evaluation complete');
    }

    // Observability: record the evaluation outcome so `why-no-proact` can
    // explain exactly which gate fired (cooldown / budget / no-signals / the
    // LLM skipping everything) instead of failing silently.
    const evalNow = Date.now();
    const reason = result.skipReason
      ?? (result.items.length === 0 && result.llmCalled ? 'llm_skipped_all' : undefined);
    ctx.db.recordProactiveDecision({
      userId,
      at: evalNow,
      stage: 'evaluate',
      outcome: result.items.length > 0 ? 'created' : 'skipped',
      reason,
      detail: {
        dial,
        signalsFound: result.signalsFound,
        itemsCreated: result.items.length,
        llmCalled: result.llmCalled,
        todayItemCount,
        budgetCap: DIAL_BUDGETS[dial],
        lastProactiveAt,
        ...(result.skipReason === 'cooldown' && lastProactiveAt
          ? { cooldownRemainingMs: Math.max(0, PROACTIVE_COOLDOWN_MS - (evalNow - lastProactiveAt)) }
          : {}),
      },
    });
  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Proactive evaluation failed');
  }
}

/**
 * B8: Clean up old sub-agent runs and their sessions
 */
export function runSubAgentCleanup(ctx: GardenerContext, cleanupAfterSeconds: number = 3600): void {
  try {
    const maxAgeMs = cleanupAfterSeconds * 1000;

    // Get child session IDs of old sub-agent runs before deleting
    const childSessionIds = ctx.db.getSubAgentChildSessionIds(maxAgeMs);

    // Delete old completed/failed sub-agent run records
    const runsDeleted = ctx.db.deleteOldSubAgentRuns(maxAgeMs);

    // Delete their sessions and messages
    let sessionsDeleted = 0;
    for (const sessionId of childSessionIds) {
      try {
        ctx.db.deleteSession(sessionId);
        sessionsDeleted++;
      } catch {
        // Session may already be deleted
      }
    }

    // Clean up stale session references in scheduled items
    let staleSessionsCleaned = 0;
    try {
      staleSessionsCleaned = ctx.db.cleanStaleScheduledItemSessions();
    } catch {
      // Non-critical — log and continue
    }

    if (runsDeleted > 0 || sessionsDeleted > 0 || staleSessionsCleaned > 0) {
      ctx.logger.info(
        { runsDeleted, sessionsDeleted, staleSessionsCleaned },
        'Sub-agent cleanup complete'
      );
    }

  } catch (err) {
    ctx.logger.warn({ error: (err as Error).message }, 'Sub-agent cleanup failed');
  }
}
