// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// scheduled-tasks repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';
import { buildVisibilityWhere } from '../../bridge/visibility.js';
import { utc } from './shared.js';

export const SCHEDULED_TASK_DISPLAY_SELECT = `
  u.name AS owner_name,
  COALESCE(
    (SELECT MIN(org_name) FROM user_gitea_orgs WHERE org_id = st.visibility_scope_org_id),
    (SELECT name FROM local_orgs WHERE id = st.visibility_scope_org_id)
  ) AS visibility_scope_org_name
`.trim();

export const SCHEDULED_TASK_DISPLAY_JOIN = `LEFT JOIN users u ON u.id = st.owner_id`;


export type ScheduledTaskKind = 'agent' | 'script';


export interface ScheduledTask {
  id: number;
  title: string | null;
  body: string;
  pieceName: string;
  profile: string;
  outputFormat: string;
  cronExpression: string;
  nextRunAt: string;
  lastRunAt: string | null;
  lastJobId: string | null;
  isActive: boolean;
  ownerId: string | null;
  ownerName?: string | null;
  visibility: 'private' | 'org' | 'public';
  visibilityScopeOrgId: string | null;
  visibilityScopeOrgName?: string | null;
  browserSessionProfileId?: number | null;
  taskKind: ScheduledTaskKind;
  scriptName: string | null;
  scriptParams: string | null; // JSON-encoded object or null
  /** 所属スペース。生成される local_task に継承される。null = owner 個人スペース。 */
  spaceId: string | null;
  createdAt: string;
  updatedAt: string;
}


export interface CreateScheduledTaskParams {
  title?: string | null;
  body: string;
  pieceName?: string;
  profile?: string;
  outputFormat?: string;
  cronExpression: string;
  nextRunAt: string;
  ownerId?: string | null;
  visibility?: 'private' | 'org' | 'public';
  visibilityScopeOrgId?: string | null;
  browserSessionProfileId?: number | null;
  taskKind?: ScheduledTaskKind;
  scriptName?: string | null;
  scriptParams?: string | null;
  spaceId?: string | null;
}


export interface UpdateScheduledTaskParams {
  title?: string;
  body?: string;
  pieceName?: string;
  profile?: string;
  outputFormat?: string;
  cronExpression?: string;
  nextRunAt?: string;
  lastRunAt?: string;
  lastJobId?: string;
  isActive?: boolean;
  visibility?: 'private' | 'org' | 'public';
  visibilityScopeOrgId?: string | null;
  browserSessionProfileId?: number | null;
  taskKind?: ScheduledTaskKind;
  scriptName?: string | null;
  scriptParams?: string | null;
}


  // ── Scheduled Tasks ──────────────────────────────────────────

  export function mapScheduledTask(row: any): ScheduledTask {
    const rawVisibility = row.visibility;
    const visibility: ScheduledTask['visibility'] =
      rawVisibility === 'org' || rawVisibility === 'public' ? rawVisibility : 'private';
    return {
      id: row.id,
      title: row.title,
      body: row.body,
      pieceName: row.piece_name,
      profile: row.profile,
      outputFormat: row.output_format,
      cronExpression: row.cron_expression,
      nextRunAt: utc(row.next_run_at),
      lastRunAt: utc(row.last_run_at),
      lastJobId: row.last_job_id,
      isActive: row.is_active === 1,
      ownerId: row.owner_id ?? null,
      ownerName: row.owner_name ?? null,
      visibility,
      visibilityScopeOrgId: row.visibility_scope_org_id ?? null,
      visibilityScopeOrgName: row.visibility_scope_org_name ?? null,
      browserSessionProfileId: row.browser_session_profile_id ?? null,
      taskKind: row.task_kind === 'script' ? 'script' : 'agent',
      scriptName: row.script_name ?? null,
      scriptParams: row.script_params ?? null,
      spaceId: row.space_id ?? null,
      createdAt: utc(row.created_at),
      updatedAt: utc(row.updated_at),
    };
  }


  export async function createScheduledTask(db: Database.Database, params: CreateScheduledTaskParams): Promise<ScheduledTask> {
    const result = db
      .prepare(
        `INSERT INTO scheduled_tasks (title, body, piece_name, profile, output_format, cron_expression, next_run_at, owner_id, visibility, visibility_scope_org_id, browser_session_profile_id, task_kind, script_name, script_params, space_id)
         VALUES (@title, @body, @pieceName, @profile, @outputFormat, @cronExpression, @nextRunAt, @ownerId, @visibility, @visibilityScopeOrgId, @browserSessionProfileId, @taskKind, @scriptName, @scriptParams, @spaceId)`
      )
      .run({
        title: params.title ?? null,
        body: params.body,
        pieceName: params.pieceName ?? 'auto',
        profile: params.profile ?? 'auto',
        outputFormat: params.outputFormat ?? 'markdown',
        cronExpression: params.cronExpression,
        nextRunAt: params.nextRunAt,
        ownerId: params.ownerId ?? null,
        visibility: params.visibility ?? 'private',
        visibilityScopeOrgId: params.visibilityScopeOrgId ?? null,
        browserSessionProfileId: params.browserSessionProfileId ?? null,
        taskKind: params.taskKind ?? 'agent',
        scriptName: params.scriptName ?? null,
        scriptParams: params.scriptParams ?? null,
        spaceId: params.spaceId ?? null,
      });
    const task = getScheduledTaskSync(db, Number(result.lastInsertRowid));
    if (!task) throw new Error('createScheduledTask: failed to load inserted task');
    return task;
  }


  export function getScheduledTaskSync(db: Database.Database, id: number): ScheduledTask | null {
    const row = db
      .prepare(`
        SELECT st.*,
               ${SCHEDULED_TASK_DISPLAY_SELECT}
        FROM scheduled_tasks st
        ${SCHEDULED_TASK_DISPLAY_JOIN}
        WHERE st.id = ?
      `)
      .get(id) as any;
    return row ? mapScheduledTask(row) : null;
  }


  export async function getScheduledTask(db: Database.Database, id: number, opts?: { viewer?: Express.User }): Promise<ScheduledTask | null> {
    const viewerClause = opts?.viewer
      ? buildVisibilityWhere(opts.viewer, 'st', { spaceColumn: 'space_id' })
      : { clause: '1=1', params: [] as unknown[] };
    const row = db
      .prepare(`
        SELECT st.*,
               ${SCHEDULED_TASK_DISPLAY_SELECT}
        FROM scheduled_tasks st
        ${SCHEDULED_TASK_DISPLAY_JOIN}
        WHERE st.id = ? AND ${viewerClause.clause}
      `)
      .get(id, ...viewerClause.params) as any;
    return row ? mapScheduledTask(row) : null;
  }


  export async function listScheduledTasks(db: Database.Database, filter?: { viewer?: Express.User; spaceId?: string | null }): Promise<ScheduledTask[]> {
    const conditions: string[] = [];
    const queryParams: unknown[] = [];
    if (filter?.viewer) {
      const w = buildVisibilityWhere(filter.viewer, 'st', { spaceColumn: 'space_id' });
      conditions.push(w.clause);
      queryParams.push(...w.params);
    }
    // Optional per-space scoping (the space's "Schedules" tab). null = only the
    // space-less (personal) schedules; a string = exactly that space.
    if (filter?.spaceId !== undefined) {
      if (filter.spaceId === null) {
        conditions.push('st.space_id IS NULL');
      } else {
        conditions.push('st.space_id = ?');
        queryParams.push(filter.spaceId);
      }
    }
    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
    const rows = db
      .prepare(`
        SELECT st.*,
               ${SCHEDULED_TASK_DISPLAY_SELECT}
        FROM scheduled_tasks st
        ${SCHEDULED_TASK_DISPLAY_JOIN}
        ${whereClause}
        ORDER BY st.created_at DESC
      `)
      .all(...queryParams) as any[];
    return rows.map(r => mapScheduledTask(r));
  }


  /**
   * due なスケジュールタスクをアトミックに claim して返す。
   * BEGIN IMMEDIATE で書き込みロックを即座に取得し、
   * 他のスケジューラーインスタンスとの重複実行を防止する。
   * claim されたタスクは next_run_at が遠い未来に設定されるため、
   * 他インスタンスに再取得されない。
   * 呼び出し側が実行後に正しい next_run_at を再設定する。
   */
  export async function getScheduledTasksDue(db: Database.Database): Promise<ScheduledTask[]> {
    // 十分遠い未来（claim マーカー）
    const claimMarker = '9999-12-31 23:59:59';

    // BEGIN IMMEDIATE: トランザクション開始時に RESERVED ロックを取得し、
    // 他の書き込みトランザクションとの競合を防ぐ
    const txn = db.transaction(() => {
      const rows = db
        .prepare(
          `SELECT * FROM scheduled_tasks
           WHERE is_active = 1 AND next_run_at <= datetime('now')
           ORDER BY next_run_at ASC`
        )
        .all() as any[];

      if (rows.length === 0) return [];

      // claim: next_run_at を遠い未来に設定して他インスタンスからの重複取得を防止
      const ids = rows.map((r: any) => r.id);
      db
        .prepare(
          `UPDATE scheduled_tasks
           SET next_run_at = ?, updated_at = datetime('now')
           WHERE id IN (${ids.map(() => '?').join(',')})`
        )
        .run(claimMarker, ...ids);

      return rows;
    });

    const rows = txn.immediate();
    return rows.map((r: any) => mapScheduledTask(r));
  }


  export async function updateScheduledTask(db: Database.Database, id: number, params: UpdateScheduledTaskParams): Promise<ScheduledTask | null> {
    const sets: string[] = [];
    const values: Record<string, any> = { id };

    if (params.title !== undefined) { sets.push('title = @title'); values.title = params.title; }
    if (params.body !== undefined) { sets.push('body = @body'); values.body = params.body; }
    if (params.pieceName !== undefined) { sets.push('piece_name = @pieceName'); values.pieceName = params.pieceName; }
    if (params.profile !== undefined) { sets.push('profile = @profile'); values.profile = params.profile; }
    if (params.outputFormat !== undefined) { sets.push('output_format = @outputFormat'); values.outputFormat = params.outputFormat; }
    if (params.cronExpression !== undefined) { sets.push('cron_expression = @cronExpression'); values.cronExpression = params.cronExpression; }
    if (params.nextRunAt !== undefined) { sets.push('next_run_at = @nextRunAt'); values.nextRunAt = params.nextRunAt; }
    if (params.lastRunAt !== undefined) { sets.push('last_run_at = @lastRunAt'); values.lastRunAt = params.lastRunAt; }
    if (params.lastJobId !== undefined) { sets.push('last_job_id = @lastJobId'); values.lastJobId = params.lastJobId; }
    if (params.isActive !== undefined) { sets.push('is_active = @isActive'); values.isActive = params.isActive ? 1 : 0; }
    if (params.visibility !== undefined) { sets.push('visibility = @visibility'); values.visibility = params.visibility; }
    if (params.visibilityScopeOrgId !== undefined) {
      sets.push('visibility_scope_org_id = @visibilityScopeOrgId');
      values.visibilityScopeOrgId = params.visibilityScopeOrgId;
    }
    if (params.browserSessionProfileId !== undefined) {
      sets.push('browser_session_profile_id = @browserSessionProfileId');
      values.browserSessionProfileId = params.browserSessionProfileId;
    }
    if (params.taskKind !== undefined) {
      sets.push('task_kind = @taskKind');
      values.taskKind = params.taskKind;
    }
    if (params.scriptName !== undefined) {
      sets.push('script_name = @scriptName');
      values.scriptName = params.scriptName;
    }
    if (params.scriptParams !== undefined) {
      sets.push('script_params = @scriptParams');
      values.scriptParams = params.scriptParams;
    }

    if (sets.length === 0) return getScheduledTaskSync(db, id);

    sets.push("updated_at = datetime('now')");
    db.prepare(`UPDATE scheduled_tasks SET ${sets.join(', ')} WHERE id = @id`).run(values);
    return getScheduledTaskSync(db, id);
  }


  export async function deleteScheduledTask(db: Database.Database, id: number): Promise<boolean> {
    const result = db.prepare('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
    return result.changes > 0;
  }
