/**
 * A2aTaskReconciler — 非 terminal な a2a_tasks を MAESTRO ジョブ状態と突き合わせて収束させる。
 *
 * 用途:
 *   - ブリッジ再起動後の stale working タスクを terminal へ収束（start() の即時 tick）。
 *   - クライアント切断で live executor が終了した場合でも、次 tick で検知して保存。
 *
 * 冪等性:
 *   - listNonTerminalA2aTasks は terminal 済タスクを返さない。
 *   - saveA2aTask は upsert なので同じ terminal を二重書きしても安全。
 */

import * as crypto from 'crypto';
import { logger } from '../../logger.js';
import type { Repository } from '../../db/repository.js';
import {
  enumerateOutputArtifacts,
  finalizeStatusFromJob,
  TERMINAL_JOB_STATUSES,
} from './task-finalize.js';
import { isDelegationLive } from './delegation.js';

export interface A2aReconcilerDeps {
  repo: Repository;
  /** Tick 間隔 (ms)。デフォルト 5000。 */
  intervalMs?: number;
  /** 現在時刻 ISO 文字列。テスト決定論化のため注入可能。 */
  now?: () => string;
}

export class A2aTaskReconciler {
  private readonly repo: Repository;
  private readonly intervalMs: number;
  private readonly now: () => string;
  private intervalHandle: ReturnType<typeof setInterval> | null = null;
  /** 再入ガード: 前の tick が終わっていない間は次の tick をスキップ */
  private running = false;

  constructor(deps: A2aReconcilerDeps) {
    this.repo = deps.repo;
    this.intervalMs = deps.intervalMs ?? 5000;
    this.now = deps.now ?? (() => new Date().toISOString());
  }

  /**
   * 非 terminal な a2a_tasks を 1 回走査して収束させる。
   * per-row try/catch で、1 行の失敗が全体を止めないようにしている。
   * 再入ガード付き: 前の tick が終わっていない間はスキップして多重実行を防ぐ。
   */
  async reconcileOnce(): Promise<{ scanned: number; finalized: number }> {
    if (this.running) return { scanned: 0, finalized: 0 };
    this.running = true;
    try {
      return await this._reconcileOnceInner();
    } finally {
      this.running = false;
    }
  }

  private async _reconcileOnceInner(): Promise<{ scanned: number; finalized: number }> {
    const rows = this.repo.listNonTerminalA2aTasks(500);
    let finalized = 0;

    for (const row of rows) {
      try {
        // enqueue 直後で jobId がまだ無い — 次 tick で拾える
        if (row.jobId === null) continue;

        const payload = row.payload as Record<string, unknown>;
        const meta = (payload.metadata as Record<string, unknown> | undefined) ?? {};

        // ── Task 6: 委任失効スイープ (restart-durable revocation sweep) ──────
        // grantId がある場合、委任が今も有効かを確認する（fail-closed）。
        // row が DB から消えている場合も失効扱いにする（fail-closed）。
        // 失効済みなら: リンクジョブをキャンセルし、タスクを canceled に収束させる。
        const grantId = meta.a2aGrantId as string | undefined;
        if (grantId) {
          const deleg = this.repo.getA2aDelegationByGrantId(grantId);
          // fail-closed: deleg が undefined（row 消失）は失効扱い
          if (!isDelegationLive(deleg ?? { expiresAt: null, revokedAt: 'revoked' }, this.now())) {
            const taskId = row.id;
            const currentState = ((payload.status as Record<string, unknown> | undefined)?.state) as string | undefined;
            // 状態一致スキップ (2C-1 churn 防止): すでに canceled なら再書き込み不要
            if (currentState === 'canceled') continue;

            // cancelA2aLinkedJob は queued/dispatching/running/waiting_human/waiting_subtasks/retry
            // のすべての非終端ステータスを対象とする。revoked 委任にリンクされたジョブは、
            // 現在の状態にかかわらず確実にキャンセルされる。A2A タスク自体もこの下で即
            // canceled に確定するため、二重スイープは発生しない（状態一致スキップが防ぐ）。
            let jobCancelled = false;
            if (row.jobId) {
              jobCancelled = this.repo.cancelA2aLinkedJob(row.jobId);
            }
            logger.info(`[a2a-reconciler] revoked-sweep taskId=${taskId} grant=${grantId} jobCancelled=${jobCancelled}`);

            payload.status = {
              state: 'canceled',
              timestamp: this.now(),
            };
            this.persistTask(row, payload, meta);
            finalized++;
            continue; // 既存の job-terminal 収束ロジックはスキップ
          }
        }
        // ─────────────────────────────────────────────────────────────────────

        const job = await this.repo.getJob(row.jobId);

        if (!job) {
          // ジョブが DB から消えている → failed で収束
          const taskId = row.id;
          const contextId = (payload.contextId as string | undefined) ?? taskId;
          // 状態一致スキップ: すでに failed なら再書き込み不要
          const currentState = ((payload.status as Record<string, unknown> | undefined)?.state) as string | undefined;
          if (currentState === 'failed') continue;
          payload.status = {
            state: 'failed',
            message: this.makeMessage('job not found', contextId, taskId),
            timestamp: this.now(),
          };
          this.persistTask(row, payload, meta);
          finalized++;
          logger.warn(`[a2a-reconciler] taskId=${taskId} job=${row.jobId} not found → finalized as failed`);
          continue;
        }

        // Live executor がまだ処理中 — 何もしない
        if (!TERMINAL_JOB_STATUSES.has(job.status)) continue;

        // Terminal ジョブ → A2A 終端状態に収束
        const { state, message } = finalizeStatusFromJob(job);
        const taskId = row.id;
        const contextId = (payload.contextId as string | undefined) ?? taskId;

        // 状態一致スキップ: すでに同じ状態なら再書き込み不要
        // （waiting_human→input-required のような非 terminal A2A 状態の churn を防止）
        const currentState = ((payload.status as Record<string, unknown> | undefined)?.state) as string | undefined;
        if (currentState === state) continue;

        if (state === 'completed') {
          // succeeded の場合: workspace を解決して artifacts を列挙
          const localTaskId = (meta.maestroLocalTaskId as number | undefined) ?? null;
          let workspacePath: string | null = null;

          if (localTaskId != null) {
            const localTask = await this.repo.getLocalTask(localTaskId);
            workspacePath = localTask?.workspacePath ?? (job.worktreePath ?? null);
          } else {
            workspacePath = job.worktreePath ?? null;
          }

          const rawArtifacts = enumerateOutputArtifacts(workspacePath);
          payload.artifacts = rawArtifacts.map(art => ({
            artifactId: art.artifactId,
            name: art.name,
            parts: [{ kind: 'text', text: art.text }],
          }));
        }

        // status フィールドを上書き（message は完了 or キャンセルでは不要）
        payload.status = {
          state,
          ...(message ? { message: this.makeMessage(message, contextId, taskId) } : {}),
          timestamp: this.now(),
        };

        this.persistTask(row, payload, meta);
        finalized++;
        logger.info(`[a2a-reconciler] taskId=${taskId} jobStatus=${job.status} → a2aState=${state}`);
      } catch (err) {
        logger.warn(`[a2a-reconciler] row=${row.id} error: ${err}`);
      }
    }

    return { scanned: rows.length, finalized };
  }

  /**
   * 即時 tick + intervalMs 毎のポーリングを開始する。
   * start() の即時 tick が再起動直後の re-sync を担う。
   * double-start ガード: 既存インターバルがあればクリアしてから張り直す。
   */
  start(): void {
    if (this.intervalHandle !== null) {
      clearInterval(this.intervalHandle);
      this.intervalHandle = null;
    }
    void this.reconcileOnce().catch(err =>
      logger.warn(`[a2a-reconciler] start tick error: ${err}`),
    );
    this.intervalHandle = setInterval(() => {
      void this.reconcileOnce().catch(err =>
        logger.warn(`[a2a-reconciler] tick error: ${err}`),
      );
    }, this.intervalMs);
  }

  /** インターバルを停止する。 */
  stop(): void {
    if (this.intervalHandle !== null) {
      clearInterval(this.intervalHandle);
      this.intervalHandle = null;
    }
  }

  // ── private helpers ───────────────────────────────────────────────────────

  /**
   * executor.ts の makeStatusMessage と同じ構造で Message オブジェクトを組み立てる。
   * A2A SDK の内部型 Message2 と構造的に同一。
   */
  private makeMessage(text: string, contextId: string, taskId: string): Record<string, unknown> {
    return {
      kind: 'message',
      messageId: crypto.randomUUID(),
      role: 'agent',
      parts: [{ kind: 'text', text }],
      contextId,
      taskId,
    };
  }

  /**
   * saveA2aTask を呼ぶ際に委任カラム（delegation_id / grant_id / acting_user_id）を保持する。
   * payload.metadata に格納されている値を task-store.ts の規約に従って写す。
   */
  private persistTask(
    row: { id: string; jobId: string | null },
    payload: Record<string, unknown>,
    meta: Record<string, unknown>,
  ): void {
    this.repo.saveA2aTask({
      id: row.id,
      contextId: (payload.contextId as string | null) ?? null,
      jobId: row.jobId,
      localTaskId: (meta.maestroLocalTaskId as number | null) ?? null,
      payload: payload as object,
      delegationId: (meta.a2aDelegationId as string | undefined) ?? undefined,
      grantId: (meta.a2aGrantId as string | undefined) ?? undefined,
      actingUserId: (meta.a2aActingUserId as string | undefined) ?? undefined,
    });
  }
}
