// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// a2a repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';

export interface A2aClientRow {
  clientId: string;
  name: string;
  redirectUris: string[];
  secretHash: string | null;
  tokenEndpointAuthMethod: string;
  agentCardUrl: string | null;
  issuer: string | null;
  keyFingerprint: string | null;
  status: string;
  createdBy: string | null;
  createdAt?: string;
}


export interface A2aDelegationRow {
  id: string;
  userId: string;
  clientId: string;
  grantId: string | null;
  grantedSpaceIds: string[];
  grantedSkills: string[];
  audience: string | null;
  expiresAt: string | null;
  revokedAt: string | null;
  createdAt?: string;
}


  export function oidcUpsert(db: Database.Database, model: string, id: string, payload: object, opts: { expiresAt?: number | null; grantId?: string | null; userCode?: string | null; uid?: string | null } = {}): void {
    db.prepare(`
      INSERT INTO oidc_models (model, id, payload, grant_id, user_code, uid, expires_at, consumed_at)
      VALUES (@model, @id, @payload, @grantId, @userCode, @uid, @expiresAt, NULL)
      ON CONFLICT(model, id) DO UPDATE SET
        payload = excluded.payload,
        grant_id = excluded.grant_id,
        user_code = excluded.user_code,
        uid = excluded.uid,
        expires_at = excluded.expires_at
    `).run({
      model, id,
      payload: JSON.stringify(payload),
      grantId: opts.grantId ?? null,
      userCode: opts.userCode ?? null,
      uid: opts.uid ?? null,
      expiresAt: opts.expiresAt ?? null,
    });
  }


  export function oidcFind(db: Database.Database, model: string, id: string): { payload: Record<string, unknown>; consumedAt: number | null } | undefined {
    const row = db.prepare(
      'SELECT payload, consumed_at FROM oidc_models WHERE model = ? AND id = ?',
    ).get(model, id) as { payload: string; consumed_at: number | null } | undefined;
    if (!row) return undefined;
    return { payload: JSON.parse(row.payload), consumedAt: row.consumed_at };
  }


  export function oidcFindByUid(db: Database.Database, uid: string): { payload: Record<string, unknown> } | undefined {
    const row = db.prepare(
      "SELECT payload FROM oidc_models WHERE model = 'Session' AND uid = ?",
    ).get(uid) as { payload: string } | undefined;
    return row ? { payload: JSON.parse(row.payload) } : undefined;
  }


  export function oidcFindByUserCode(db: Database.Database, userCode: string): { payload: Record<string, unknown> } | undefined {
    const row = db.prepare(
      'SELECT payload FROM oidc_models WHERE user_code = ?',
    ).get(userCode) as { payload: string } | undefined;
    return row ? { payload: JSON.parse(row.payload) } : undefined;
  }


  export function oidcConsume(db: Database.Database, model: string, id: string, consumedAt: number): void {
    db.prepare('UPDATE oidc_models SET consumed_at = ? WHERE model = ? AND id = ?')
      .run(consumedAt, model, id);
  }


  export function oidcDestroy(db: Database.Database, model: string, id: string): void {
    db.prepare('DELETE FROM oidc_models WHERE model = ? AND id = ?').run(model, id);
  }


  export function oidcRevokeByGrantId(db: Database.Database, grantId: string): void {
    db.prepare('DELETE FROM oidc_models WHERE grant_id = ?').run(grantId);
  }


  export function createA2aClient(db: Database.Database, row: A2aClientRow): void {
    db.prepare(`
      INSERT INTO a2a_clients
        (client_id, name, redirect_uris, secret_hash, token_endpoint_auth_method,
         agent_card_url, issuer, key_fingerprint, status, created_by)
      VALUES (@clientId, @name, @redirectUris, @secretHash, @tokenEndpointAuthMethod,
              @agentCardUrl, @issuer, @keyFingerprint, @status, @createdBy)
    `).run({
      ...row,
      redirectUris: JSON.stringify(row.redirectUris),
    });
  }


  export function getA2aClient(db: Database.Database, clientId: string): A2aClientRow | undefined {
    const r = db.prepare('SELECT * FROM a2a_clients WHERE client_id = ?').get(clientId) as any;
    if (!r) return undefined;
    return {
      clientId: r.client_id, name: r.name, redirectUris: JSON.parse(r.redirect_uris),
      secretHash: r.secret_hash, tokenEndpointAuthMethod: r.token_endpoint_auth_method,
      agentCardUrl: r.agent_card_url, issuer: r.issuer, keyFingerprint: r.key_fingerprint,
      status: r.status, createdBy: r.created_by, createdAt: r.created_at,
    };
  }


  export function listA2aClients(db: Database.Database): A2aClientRow[] {
    const rows = db.prepare('SELECT * FROM a2a_clients ORDER BY created_at DESC').all() as any[];
    return rows.map(r => ({
      clientId: r.client_id, name: r.name, redirectUris: JSON.parse(r.redirect_uris),
      secretHash: r.secret_hash, tokenEndpointAuthMethod: r.token_endpoint_auth_method,
      agentCardUrl: r.agent_card_url, issuer: r.issuer, keyFingerprint: r.key_fingerprint,
      status: r.status, createdBy: r.created_by, createdAt: r.created_at,
    }));
  }


  export function setA2aClientStatus(db: Database.Database, clientId: string, status: 'active' | 'disabled'): void {
    db.prepare('UPDATE a2a_clients SET status = ? WHERE client_id = ?').run(status, clientId);
  }


  export function createA2aDelegation(db: Database.Database, row: A2aDelegationRow): void {
    db.prepare(`
      INSERT INTO a2a_delegations
        (id, user_id, client_id, grant_id, granted_space_ids, granted_skills, audience, expires_at, revoked_at)
      VALUES (@id, @userId, @clientId, @grantId, @grantedSpaceIds, @grantedSkills, @audience, @expiresAt, @revokedAt)
    `).run({
      id: row.id, userId: row.userId, clientId: row.clientId, grantId: row.grantId,
      grantedSpaceIds: JSON.stringify(row.grantedSpaceIds),
      grantedSkills: JSON.stringify(row.grantedSkills),
      audience: row.audience, expiresAt: row.expiresAt, revokedAt: row.revokedAt,
    });
  }


  export function mapDelegation(r: any): A2aDelegationRow {
    return {
      id: r.id, userId: r.user_id, clientId: r.client_id, grantId: r.grant_id,
      grantedSpaceIds: JSON.parse(r.granted_space_ids || '[]'),
      grantedSkills: JSON.parse(r.granted_skills || '[]'),
      audience: r.audience, expiresAt: r.expires_at, revokedAt: r.revoked_at, createdAt: r.created_at,
    };
  }


  export function getA2aDelegationByGrantId(db: Database.Database, grantId: string): A2aDelegationRow | undefined {
    const r = db.prepare('SELECT * FROM a2a_delegations WHERE grant_id = ?').get(grantId);
    return r ? mapDelegation(r) : undefined;
  }


  export function listA2aDelegationsForUser(db: Database.Database, userId: string): A2aDelegationRow[] {
    return (db.prepare('SELECT * FROM a2a_delegations WHERE user_id = ? ORDER BY created_at DESC').all(userId) as any[])
      .map(r => mapDelegation(r));
  }


  export function revokeA2aDelegation(db: Database.Database, id: string, revokedAtIso: string): void {
    // revoked_at IS NULL ガード: 二重失効で元の失効時刻を上書きしない（監査の forensic 記録を守る）
    db
      .prepare('UPDATE a2a_delegations SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL')
      .run(revokedAtIso, id);
  }


  export function getA2aDelegationById(db: Database.Database, id: string): A2aDelegationRow | undefined {
    const r = db.prepare('SELECT * FROM a2a_delegations WHERE id = ?').get(id);
    return r ? mapDelegation(r) : undefined;
  }


  export function listLiveA2aDelegationsForClient(db: Database.Database, clientId: string, nowIso: string): A2aDelegationRow[] {
    const rows = db.prepare(
      `SELECT * FROM a2a_delegations
        WHERE client_id = ? AND revoked_at IS NULL
          AND (expires_at IS NULL OR expires_at > ?)
        ORDER BY created_at DESC`,
    ).all(clientId, nowIso);
    return (rows as any[]).map((r) => mapDelegation(r));
  }


  export function listNonTerminalA2aTasksByGrant(db: Database.Database, grantId: string, limit = 500): Array<{ id: string; jobId: string | null; payload: Record<string, unknown> }> {
    const rows = db.prepare(
      `SELECT id, job_id, payload FROM a2a_tasks
        WHERE grant_id = ?
          AND (json_extract(payload, '$.status.state') NOT IN
               ('completed','failed','canceled','rejected')
               OR json_extract(payload, '$.status.state') IS NULL)
        ORDER BY updated_at
        LIMIT ?`,
    ).all(grantId, limit);
    return (rows as any[]).map((r) => ({
      id: r.id,
      jobId: r.job_id ?? null,
      payload: JSON.parse(r.payload ?? '{}'),
    }));
  }


  export function countNonTerminalA2aTasksByGrant(db: Database.Database, grantId: string): number {
    // Concurrency cap counts tasks that currently (or imminently) consume a job slot.
    // 'input-required' is excluded ONLY while its linked job is genuinely parked
    // (waiting_human) or gone — a parked task holds no live worker/LLM capacity, so it must
    // not block new task creation. But if the MAESTRO-side human answers the ASK, the job
    // resumes (waiting_human → running) while the a2a_tasks payload may still read
    // 'input-required'; that resumed task DOES consume capacity and must be counted.
    // Keying the exclusion on the live job status (via LEFT JOIN) instead of the frozen
    // payload state closes a concurrency-cap bypass by ASK-park-then-resume.
    // NOTE: listNonTerminalA2aTasksByGrant intentionally keeps the 4-state exclusion (no
    // input-required) so the revocation cascade can still find and cancel parked tasks.
    const row = db.prepare(
      `SELECT COUNT(*) AS n FROM a2a_tasks t
         LEFT JOIN jobs j ON j.id = t.job_id
        WHERE t.grant_id = ?
          AND (json_extract(t.payload, '$.status.state') NOT IN
               ('completed','failed','canceled','rejected')
               OR json_extract(t.payload, '$.status.state') IS NULL)
          AND NOT (json_extract(t.payload, '$.status.state') = 'input-required'
                   AND COALESCE(j.status, 'waiting_human') = 'waiting_human')`,
    ).get(grantId);
    return (row as { n: number }).n;
  }


  export function cancelA2aLinkedJob(db: Database.Database, jobId: string): boolean {
    const res = db.prepare(
      `UPDATE jobs SET status = 'cancelled', updated_at = datetime('now')
        WHERE id = ? AND status IN
          ('queued','dispatching','running','waiting_human','waiting_subtasks','retry')`,
    ).run(jobId);
    return res.changes > 0;
  }


  export function listA2aDelegationsForUserWithClient(db: Database.Database, userId: string): Array<A2aDelegationRow & { clientName: string | null; clientStatus: string | null }> {
    const rows = db.prepare(
      `SELECT d.*, c.name AS client_name, c.status AS client_status
         FROM a2a_delegations d
         LEFT JOIN a2a_clients c ON c.client_id = d.client_id
        WHERE d.user_id = ?
        ORDER BY d.created_at DESC`,
    ).all(userId);
    return (rows as any[]).map((r) => ({
      ...mapDelegation(r),
      clientName: r.client_name ?? null,
      clientStatus: r.client_status ?? null,
    }));
  }


  export function listA2aDelegationsAllWithClient(db: Database.Database): Array<
    A2aDelegationRow & { userId: string; clientName: string | null; clientStatus: string | null }
  > {
    const rows = db.prepare(
      `SELECT d.*, c.name AS client_name, c.status AS client_status
         FROM a2a_delegations d
         LEFT JOIN a2a_clients c ON c.client_id = d.client_id
        ORDER BY d.created_at DESC`,
    ).all();
    return (rows as any[]).map((r) => ({
      ...mapDelegation(r),
      clientName: r.client_name ?? null,
      clientStatus: r.client_status ?? null,
    }));
  }


  export function saveA2aTask(db: Database.Database, row: {
    id: string;
    contextId: string | null;
    jobId: string | null;
    localTaskId: number | null;
    payload: object;
    delegationId?: string;
    grantId?: string;
    actingUserId?: string;
  }): void {
    db.prepare(`
      INSERT INTO a2a_tasks (id, context_id, job_id, local_task_id, payload, updated_at, delegation_id, grant_id, acting_user_id)
      VALUES (@id, @contextId, @jobId, @localTaskId, @payload, datetime('now'), @delegationId, @grantId, @actingUserId)
      ON CONFLICT(id) DO UPDATE SET
        context_id = excluded.context_id, job_id = excluded.job_id,
        local_task_id = excluded.local_task_id, payload = excluded.payload, updated_at = datetime('now'),
        delegation_id = excluded.delegation_id, grant_id = excluded.grant_id, acting_user_id = excluded.acting_user_id
    `).run({
      id: row.id,
      contextId: row.contextId,
      jobId: row.jobId,
      localTaskId: row.localTaskId,
      payload: JSON.stringify(row.payload),
      delegationId: row.delegationId ?? null,
      grantId: row.grantId ?? null,
      actingUserId: row.actingUserId ?? null,
    });
  }


  export function loadA2aTask(db: Database.Database, id: string): { payload: Record<string, unknown> } | undefined {
    const r = db.prepare('SELECT payload FROM a2a_tasks WHERE id = ?').get(id) as { payload: string } | undefined;
    return r ? { payload: JSON.parse(r.payload) } : undefined;
  }


  export function getA2aTaskByJobId(db: Database.Database, jobId: string): { id: string; payload: Record<string, unknown> } | undefined {
    const r = db.prepare('SELECT id, payload FROM a2a_tasks WHERE job_id = ?').get(jobId) as { id: string; payload: string } | undefined;
    return r ? { id: r.id, payload: JSON.parse(r.payload) } : undefined;
  }


  export function listNonTerminalA2aTasks(db: Database.Database, limit = 500): Array<{ id: string; jobId: string | null; payload: Record<string, unknown> }> {
    const rows = db.prepare(`
      SELECT id, job_id, payload FROM a2a_tasks
      WHERE json_extract(payload, '$.status.state') NOT IN ('completed', 'failed', 'canceled', 'rejected')
         OR json_extract(payload, '$.status.state') IS NULL
      ORDER BY updated_at
      LIMIT ?
    `).all(limit) as Array<{ id: string; job_id: string | null; payload: string }>;
    return rows.map(r => ({ id: r.id, jobId: r.job_id, payload: JSON.parse(r.payload) }));
  }
