/**
 * Tracker ↔ fleet bridge (§10 / §24.5) — the in-process, no-shell-hop round-trip.
 *
 * `ingestItemAsJob` turns a tracker Item into a fleet job submitted through the
 * coordinator (so it is scheduled by the §7 router like any other job), reusing the
 * existing `trackerItemId` field + a stable idempotency-key so re-ingest dedupes.
 *
 * `echoJobToItem` mirrors the job's lifecycle back onto the Item (status + a
 * metrics-only comment). Echo is BEST-EFFORT and downstream: an items-write failure
 * is surfaced as `{ echoed: null, error }` and is NEVER allowed to fail the job.
 *
 * This module calls the items + comments + fleet repositories DIRECTLY — no HTTP,
 * no Fastify types here.
 */

import { randomUUID } from 'node:crypto';
import { NotFoundError } from '../../lib/errors.js';
import * as itemsRepo from '../items/repository.js';
import * as commentsRepo from '../comments/repository.js';
import type { ItemStatus, TrackerItemDoc } from '../items/types.js';
import * as repo from './repository.js';
import * as coordinator from './coordinator.js';
import type { SubmitResult } from './coordinator.js';
import {
  FLEET_ENGINE_CLASSES,
  FLEET_PRIORITIES,
  SubmitJobSchema,
  type FleetEngineClass,
  type FleetJobDoc,
  type FleetPriority,
  type FleetStage,
} from './types.js';

/** Minimal logger shape (pino/req.log compatible) — keeps Fastify out of this module. */
export interface BridgeLogger {
  error: (obj: unknown, msg?: string) => void;
}

/** Config flag (default OFF): auto-echo on server-side stage transitions. */
export function trackerEchoEnabled(): boolean {
  const v = (process.env.FLEET_TRACKER_ECHO ?? '').trim().toLowerCase();
  return v === '1' || v === 'true' || v === 'yes' || v === 'on';
}

// ── Item → job ────────────────────────────────────────────────────────────────

interface LabelHints {
  priority?: FleetPriority;
  engineClass?: FleetEngineClass;
  profile?: string;
  capabilities: string[];
}

/** Parse manifest hints carried as Item labels: engine-class:/profile:/priority:/cap:. */
function parseLabelHints(labels: string[]): LabelHints {
  const hints: LabelHints = { capabilities: [] };
  for (const raw of labels) {
    const label = raw.trim();
    if (label.startsWith('engine-class:')) {
      const v = label.slice('engine-class:'.length);
      if ((FLEET_ENGINE_CLASSES as readonly string[]).includes(v))
        hints.engineClass = v as FleetEngineClass;
    } else if (label.startsWith('profile:')) {
      hints.profile = label.slice('profile:'.length);
    } else if (label.startsWith('priority:')) {
      const v = label.slice('priority:'.length);
      if ((FLEET_PRIORITIES as readonly string[]).includes(v)) hints.priority = v as FleetPriority;
    } else if (label.startsWith('cap:')) {
      const v = label.slice('cap:'.length);
      if (v) hints.capabilities.push(v);
    }
  }
  return hints;
}

function coerceItemPriority(p: string): FleetPriority | undefined {
  return (FLEET_PRIORITIES as readonly string[]).includes(p) ? (p as FleetPriority) : undefined;
}

/** Stable, idempotent derived key so re-ingesting the same Item never duplicates. */
export function trackerIdempotencyKey(itemId: string): string {
  return `tracker-${itemId}`;
}

export interface IngestOptions {
  /** Override the derived priority (else label hint, else the Item's own priority). */
  priority?: FleetPriority;
}

/**
 * Read a tracker Item and submit it as a fleet job (idempotent on
 * `tracker-<itemId>`). Foreign productId / unknown item ⇒ NotFoundError.
 */
export async function ingestItemAsJob(
  productId: string,
  itemId: string,
  opts?: IngestOptions
): Promise<SubmitResult> {
  const item = await itemsRepo.getById(itemId);
  if (!item || item.productId !== productId) {
    throw new NotFoundError(`tracker item '${itemId}' not found`);
  }

  const hints = parseLabelHints(item.labels);
  const priority =
    opts?.priority ?? hints.priority ?? coerceItemPriority(item.priority) ?? 'medium';
  const description = item.description?.trim() ?? '';
  const bodyMd = description ? `# ${item.title}\n\n${item.description}` : `# ${item.title}`;

  const input = SubmitJobSchema.parse({
    idempotencyKey: trackerIdempotencyKey(itemId),
    bodyMd,
    priority,
    capabilities: hints.capabilities,
    engineClass: hints.engineClass,
    profile: hints.profile,
    trackerItemId: itemId,
  });

  return coordinator.submitJob(productId, input);
}

// ── Job → Item (one-way echo, §24.5) ───────────────────────────────────────────

export interface EchoResult {
  echoed: ItemStatus | null;
  error?: string;
}

/**
 * Map a fleet stage to a tracker Item status (full lifecycle, both directions).
 * The Item vocabulary has no `blocked`, so a terminal failure maps to `wont_fix`.
 */
export function stageToItemStatus(stage: FleetStage): ItemStatus {
  switch (stage) {
    case 'draft':
      // Saved but not submitted — not started yet.
      return 'open';
    case 'shipped':
      return 'done';
    case 'failed':
    case 'dead_letter':
      return 'wont_fix';
    default:
      // queued | blocked | assigned | building | review | testing
      return 'in_progress';
  }
}

async function writeMetricsComment(
  job: FleetJobDoc,
  item: TrackerItemDoc,
  status: ItemStatus
): Promise<void> {
  const runs = await repo.listRunsByJob(job.id);
  const latest = runs[runs.length - 1];
  // Metrics ONLY — never the prompt body (job.bodyMd) or any secret.
  const parts: string[] = [
    `agent-queue fleet: job ${job.id} → ${job.stage} (item status=${status}).`,
    `attempts=${job.attempts}.`,
  ];
  if (latest?.startedAt && latest?.endedAt) {
    const dur = Math.max(
      0,
      Math.round((Date.parse(latest.endedAt) - Date.parse(latest.startedAt)) / 1000)
    );
    if (Number.isFinite(dur)) parts.push(`duration=${dur}s.`);
  }
  const ins = latest?.insights;
  if (ins) {
    if (ins.tokensIn !== undefined || ins.tokensOut !== undefined) {
      parts.push(`tokens=${ins.tokensIn ?? 0}/${ins.tokensOut ?? 0}.`);
    }
    if (ins.costUsd !== undefined) parts.push(`cost_usd=${ins.costUsd}.`);
  }
  const now = new Date().toISOString();
  await commentsRepo.create({
    id: `cmt_${randomUUID()}`,
    itemId: item.id,
    productId: item.productId,
    authorId: 'fleet-coordinator',
    authorEmail: null,
    body: parts.join(' '),
    createdAt: now,
    updatedAt: now,
  });
  await itemsRepo.incrementCommentCount(item.id, 1);
}

/**
 * Echo a job's current stage onto its linked Item (status + metrics comment).
 * No trackerItemId ⇒ `{ echoed: null }`. Idempotent: an unchanged outcome is a
 * no-op. An items-write failure is non-fatal ⇒ `{ echoed: null, error }`.
 * A missing/foreign job throws NotFoundError (productId isolation).
 */
export async function echoJobToItem(
  productId: string,
  jobId: string,
  log?: BridgeLogger
): Promise<EchoResult> {
  const job = await repo.getJob(jobId, productId);
  if (!job) throw new NotFoundError(`fleet job '${jobId}' not found`);
  if (!job.trackerItemId) return { echoed: null };

  const status = stageToItemStatus(job.stage);
  if (job.trackerEchoedStatus === status) return { echoed: status };

  try {
    const item = await itemsRepo.getById(job.trackerItemId);
    if (!item || item.productId !== productId) {
      return { echoed: null, error: `linked item '${job.trackerItemId}' not found for product` };
    }
    await itemsRepo.update(job.trackerItemId, { status });
    await writeMetricsComment(job, item, status);
    await repo.setTrackerEchoedStatus(jobId, productId, status);
    return { echoed: status };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    log?.error(
      { jobId, itemId: job.trackerItemId, error: message },
      'fleet tracker echo failed (non-fatal)'
    );
    return { echoed: null, error: message };
  }
}

/**
 * Opt-in (FLEET_TRACKER_ECHO) best-effort echo invoked after a server-side stage
 * transition. Flag OFF ⇒ no-op (zero items writes). Never throws into the caller's
 * transition path.
 */
export async function maybeEchoOnTransition(
  productId: string,
  jobId: string,
  log?: BridgeLogger
): Promise<void> {
  if (!trackerEchoEnabled()) return;
  try {
    await echoJobToItem(productId, jobId, log);
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    log?.error({ jobId, error: message }, 'fleet tracker auto-echo failed (non-fatal)');
  }
}
