// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// calendar repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';
import { buildVisibilityWhere } from '../../bridge/visibility.js';
import { spaceColor } from '../../spaces/space-color.js';
import * as spacesRepo from './spaces.js';

/**
 * Allowed widget kinds. 'markdown' is the original Side Info Panel widget
 * (PR #308). 'node-status' was added in Phase B (2026-05) and ignores
 * markdown_content — it renders BackendStatusRegistry data live. The union
 * intentionally lives here so the API, tools and UI all share a single
 * source of truth.
 */
/**
 * スペース・カレンダーの予定エントリ（passive。実行はしない）。
 * spec: docs/superpowers/specs/2026-06-19-space-calendar-design.md §データモデル
 */
export interface CalendarEvent {
  id: number;
  spaceId: string;
  ownerId: string | null;
  date: string;               // 開始日 YYYY-MM-DD（ローカル日付）
  endDate: string | null;     // 終了日 YYYY-MM-DD。null = date と同じ（単日）
  time: string | null;        // 開始 HH:MM。null = 終日
  endTime: string | null;     // 終了 HH:MM。null = 終了時刻なし（time が null なら常に null）
  title: string;
  description: string | null;
  createdBy: 'user' | 'agent';
  sourceTaskId: number | null;
  createdAt: string;
  updatedAt: string;
}


export interface CalendarEventRow {
  id: number;
  space_id: string;
  owner_id: string | null;
  date: string;
  end_date: string | null;
  time: string | null;
  end_time: string | null;
  title: string;
  description: string | null;
  created_by: string;
  source_task_id: number | null;
  created_at: string;
  updated_at: string;
}


/**
 * [start, end]（両端含む）のローカル暦日を 'YYYY-MM-DD' 配列で返す。
 * end < start は空配列。複数日イベントを各日に展開する集計で使う。
 */
export function enumerateLocalDays(start: string, end: string): string[] {
  if (end < start) return [];
  const out: string[] = [];
  const [ys, ms, ds] = start.split('-').map(Number);
  const [ye, me, de] = end.split('-').map(Number);
  let cur = Date.UTC(ys, ms - 1, ds);
  const last = Date.UTC(ye, me - 1, de);
  // 安全弁: 異常な範囲で無限ループしないよう上限を設ける（最大 ~2 年）。
  for (let i = 0; cur <= last && i < 800; i++) {
    const d = new Date(cur);
    out.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`);
    cur += 86_400_000;
  }
  return out;
}


export function rowToCalendarEvent(row: CalendarEventRow): CalendarEvent {
  return {
    id: row.id,
    spaceId: row.space_id,
    ownerId: row.owner_id ?? null,
    date: row.date,
    endDate: row.end_date ?? null,
    time: row.time ?? null,
    endTime: row.end_time ?? null,
    title: row.title,
    description: row.description ?? null,
    createdBy: row.created_by === 'agent' ? 'agent' : 'user',
    sourceTaskId: row.source_task_id ?? null,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}


/** カレンダー月ビューの1日分の集計カウント。 */
export interface CalendarDayCounts {
  taskCount: number;
  eventCount: number;
}


/** カレンダー日詳細のタスク行（軽量・最新 job 状態付き）。 */
export interface CalendarDayTask {
  id: number;
  title: string;
  state: string;
  status: string | null;   // 最新 job の status（無ければ null）
  createdAt: string;
}


/** 横断カレンダーの凡例 1 行（スペースとその表示色）。 */
export interface CrossCalendarSpace {
  id: string;
  name: string;
  color: string;            // brand_color、無ければ spaceColor(id) で決定論的に導出
}


/** 横断カレンダー月ビュー。`days[date][spaceId]` = そのスペースのその日の集計。 */
export interface CrossSpaceCalendarMonth {
  days: Record<string, Record<string, CalendarDayCounts>>;
  events: CalendarEvent[];  // 各イベントは spaceId を持つ（CalendarEvent.spaceId）
  spaces: CrossCalendarSpace[];
}


  // ── スペース・カレンダー（予定）─────────────────────────────────────
  //
  // CRUD はスペース id でスコープ。可視性判定は呼び出し側（space-api が
  // getSpace({viewer}) で先にゲートする）に委ねる。日付集計はローカル TZ
  // オフセット（分）を SQLite の date(...,'±N minutes') 修飾子で適用する
  // （Usage ダッシュボードの localDayOf と同じ「UTC instant をオフセット分
  // ずらして暦日に落とす」方式の SQL 版）。

  /** ローカル TZ オフセット（分）を SQLite の date() 修飾子文字列に変換する。 */
  export function tzModifier(tzOffsetMin: number): string {
    const n = Math.trunc(tzOffsetMin);
    return `${n >= 0 ? '+' : '-'}${Math.abs(n)} minutes`;
  }


  export async function createCalendarEvent(db: Database.Database, params: {
    spaceId: string;
    ownerId?: string | null;
    date: string;
    endDate?: string | null;
    time?: string | null;
    endTime?: string | null;
    title: string;
    description?: string | null;
    createdBy?: 'user' | 'agent';
    sourceTaskId?: number | null;
  }): Promise<CalendarEvent> {
    // 終了日が開始日と同じ（または無効）なら単日として NULL に正規化する。
    const endDate = params.endDate && params.endDate > params.date ? params.endDate : null;
    const time = params.time ?? null;
    // 終了時刻は開始時刻があるときだけ意味を持つ。開始が終日なら end_time は捨てる。
    const endTime = time ? (params.endTime ?? null) : null;
    const result = db
      .prepare(
        `INSERT INTO calendar_events (space_id, owner_id, date, end_date, time, end_time, title, description, created_by, source_task_id)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
      )
      .run(
        params.spaceId,
        params.ownerId ?? null,
        params.date,
        endDate,
        time,
        endTime,
        params.title,
        params.description ?? null,
        params.createdBy ?? 'user',
        params.sourceTaskId ?? null,
      );
    const row = db
      .prepare(`SELECT * FROM calendar_events WHERE id = ?`)
      .get(Number(result.lastInsertRowid)) as CalendarEventRow;
    return rowToCalendarEvent(row);
  }


  export async function listCalendarEvents(db: Database.Database, spaceId: string, range?: { from?: string; to?: string }): Promise<CalendarEvent[]> {
    const conditions = ['space_id = ?'];
    const args: unknown[] = [spaceId];
    // 複数日対応: イベント [date, COALESCE(end_date,date)] が [from, to] と
    // 重なるものを返す（単日 end_date=NULL は date 自身）。
    if (range?.from) {
      conditions.push('COALESCE(end_date, date) >= ?');
      args.push(range.from);
    }
    if (range?.to) {
      conditions.push('date <= ?');
      args.push(range.to);
    }
    const rows = db
      .prepare(
        `SELECT * FROM calendar_events WHERE ${conditions.join(' AND ')}
         ORDER BY date ASC, (time IS NULL) DESC, time ASC, id ASC`
      )
      .all(...args) as CalendarEventRow[];
    return rows.map(rowToCalendarEvent);
  }


  export async function getCalendarEvent(db: Database.Database, eventId: number): Promise<CalendarEvent | null> {
    const row = db
      .prepare(`SELECT * FROM calendar_events WHERE id = ?`)
      .get(eventId) as CalendarEventRow | undefined;
    return row ? rowToCalendarEvent(row) : null;
  }


  export async function updateCalendarEvent(db: Database.Database, eventId: number, fields: { date?: string; endDate?: string | null; time?: string | null; endTime?: string | null; title?: string; description?: string | null }): Promise<CalendarEvent | null> {
    const sets: string[] = [];
    const args: unknown[] = [];
    if (fields.date !== undefined) {
      sets.push('date = ?');
      args.push(fields.date);
    }
    if (fields.endDate !== undefined) {
      sets.push('end_date = ?');
      args.push(fields.endDate);
    }
    // time / end_time は連動。開始を終日(null)にしたら end_time も必ず null に落とす。
    if (fields.time !== undefined || fields.endTime !== undefined) {
      const existing = await getCalendarEvent(db, eventId);
      const newTime = fields.time !== undefined ? fields.time : (existing?.time ?? null);
      let newEndTime = fields.endTime !== undefined ? fields.endTime : (existing?.endTime ?? null);
      if (newTime == null) newEndTime = null;
      if (fields.time !== undefined) {
        sets.push('time = ?');
        args.push(newTime);
      }
      if (newEndTime !== (existing?.endTime ?? null)) {
        sets.push('end_time = ?');
        args.push(newEndTime);
      }
    }
    if (fields.title !== undefined) {
      sets.push('title = ?');
      args.push(fields.title);
    }
    if (fields.description !== undefined) {
      sets.push('description = ?');
      args.push(fields.description);
    }
    if (sets.length === 0) return getCalendarEvent(db, eventId);
    sets.push(`updated_at = datetime('now')`);
    args.push(eventId);
    db.prepare(`UPDATE calendar_events SET ${sets.join(', ')} WHERE id = ?`).run(...args);
    return getCalendarEvent(db, eventId);
  }


  export async function deleteCalendarEvent(db: Database.Database, eventId: number): Promise<void> {
    db.prepare(`DELETE FROM calendar_events WHERE id = ?`).run(eventId);
  }


  /**
   * 月ビューの日別カウント。`days` は date → {taskCount, eventCount}。
   * タスクは local_tasks を space_id で絞り、created_at（UTC）をローカル TZ で
   * 暦日にバケツ化した日付が [monthStart, monthEnd] に入るものを数える。
   * イベントは date 列がそのまま暦日（ローカル）なので素直に範囲で絞る。
   * ファイル数は重いので含めない（日詳細でのみ算出）。
   */
  /**
   * SQL scope for "tasks belonging to this space's calendar". Normally just
   * `space_id = ?`. A PERSONAL space also owns its owner's space-less tasks:
   * personal-workspace tasks are stored with `space_id = NULL` (by design), so
   * include `(space_id IS NULL AND owner_id = ?)` when a personalOwnerId is
   * given. Mirrors worker.ts resolveToolSpaceId (NULL+owner → personal space).
   */
  export function calendarTaskScope(spaceId: string, personalOwnerId?: string | null, alias = ''): { clause: string; params: unknown[] } {
    const col = alias ? `${alias}.` : '';
    if (personalOwnerId) {
      return {
        clause: `(${col}space_id = ? OR (${col}space_id IS NULL AND ${col}owner_id = ?))`,
        params: [spaceId, personalOwnerId],
      };
    }
    return { clause: `${col}space_id = ?`, params: [spaceId] };
  }


  export async function getSpaceCalendarMonth(db: Database.Database, spaceId: string, opts: { monthStart: string; monthEnd: string; tzOffsetMin: number; personalOwnerId?: string | null }): Promise<{ days: Record<string, CalendarDayCounts>; events: CalendarEvent[] }> {
    const mod = tzModifier(opts.tzOffsetMin);
    const days: Record<string, CalendarDayCounts> = {};
    const ensure = (d: string): CalendarDayCounts => {
      if (!days[d]) days[d] = { taskCount: 0, eventCount: 0 };
      return days[d]!;
    };

    const scope = calendarTaskScope(spaceId, opts.personalOwnerId);
    const taskRows = db
      .prepare(
        `SELECT date(created_at, ?) AS local_day, COUNT(*) AS c
         FROM local_tasks
         WHERE ${scope.clause} AND date(created_at, ?) BETWEEN ? AND ?
         GROUP BY local_day`
      )
      .all(mod, ...scope.params, mod, opts.monthStart, opts.monthEnd) as Array<{ local_day: string; c: number }>;
    for (const r of taskRows) ensure(r.local_day).taskCount = r.c;

    const events = await listCalendarEvents(db, spaceId, { from: opts.monthStart, to: opts.monthEnd });
    // 複数日イベントは、またがる各日（当月内にクリップ）に 1 件ずつ計上する。
    for (const ev of events) {
      const start = ev.date < opts.monthStart ? opts.monthStart : ev.date;
      const end = (ev.endDate ?? ev.date) > opts.monthEnd ? opts.monthEnd : (ev.endDate ?? ev.date);
      for (const d of enumerateLocalDays(start, end)) ensure(d).eventCount += 1;
    }
    return { days, events };
  }


  /**
   * 日詳細。tasks = その日（ローカル暦日）に作成された local_tasks（最新 job
   * 状態付き）、events = その date の calendar_events。ファイル列挙は API 層
   * （fs スキャン）で行う。
   */
  export async function getSpaceCalendarDay(db: Database.Database, spaceId: string, date: string, tzOffsetMin: number, personalOwnerId?: string | null): Promise<{ tasks: CalendarDayTask[]; events: CalendarEvent[] }> {
    const mod = tzModifier(tzOffsetMin);
    const scope = calendarTaskScope(spaceId, personalOwnerId, 'lt');
    const taskRows = db
      .prepare(
        `SELECT lt.id AS id, lt.title AS title, lt.state AS state, lt.created_at AS created_at,
                (SELECT j.status FROM jobs j
                   WHERE j.repo = 'local/task-' || lt.id
                   ORDER BY j.created_at DESC LIMIT 1) AS status
         FROM local_tasks lt
         WHERE ${scope.clause} AND date(lt.created_at, ?) = ?
         ORDER BY lt.created_at ASC, lt.id ASC`
      )
      .all(...scope.params, mod, date) as Array<{
        id: number; title: string; state: string; created_at: string; status: string | null;
      }>;
    const tasks: CalendarDayTask[] = taskRows.map((r) => ({
      id: r.id,
      title: r.title,
      state: r.state,
      status: r.status ?? null,
      createdAt: r.created_at,
    }));
    const events = await listCalendarEvents(db, spaceId, { from: date, to: date });
    return { tasks, events };
  }


  /**
   * 横断（全スペース）月ビュー。viewer が見られるスペースだけを対象に、
   * 日 × スペースの {taskCount, eventCount} を集計する。可視性は spaces
   * 一覧と同じ `buildVisibilityWhere`（listSpaces 経由）で絞るので、
   * 見えないスペースのカウントもイベントも一切返さない。
   *
   * クエリ本数を抑えるため、対象 space_id を IN (...) でまとめて 2 本に集約する：
   *   1) local_tasks を space_id + ローカル暦日で GROUP BY
   *   2) calendar_events を space_id + date で GROUP BY
   * さらに当月のイベント本体を 1 本でまとめて取得する（各 CalendarEvent は
   * spaceId を持つので UI 側でスペース別に振り分けられる）。
   */
  export async function getCrossSpaceCalendarMonth(db: Database.Database, viewer: Express.User, opts: { monthStart: string; monthEnd: string; tzOffsetMin: number }): Promise<CrossSpaceCalendarMonth> {
    // 可視スペースの列挙は spaces 一覧とまったく同じ可視性判定を流用する。
    const visibleSpaces = await spacesRepo.listSpaces(db, { viewer });
    const spaces: CrossCalendarSpace[] = visibleSpaces.map((s) => ({
      id: s.id,
      name: s.title,
      color: s.brandColor ?? spaceColor(s.id),
    }));

    const days: Record<string, Record<string, CalendarDayCounts>> = {};
    if (spaces.length === 0) {
      return { days, events: [], spaces };
    }

    const ids = spaces.map((s) => s.id);
    const placeholders = ids.map(() => '?').join(',');
    const mod = tzModifier(opts.tzOffsetMin);
    const ensure = (date: string, spaceId: string): CalendarDayCounts => {
      const byDay = (days[date] ??= {});
      return (byDay[spaceId] ??= { taskCount: 0, eventCount: 0 });
    };

    const taskRows = db
      .prepare(
        `SELECT space_id AS sid, date(created_at, ?) AS local_day, COUNT(*) AS c
         FROM local_tasks
         WHERE space_id IN (${placeholders}) AND date(created_at, ?) BETWEEN ? AND ?
         GROUP BY sid, local_day`
      )
      .all(mod, ...ids, mod, opts.monthStart, opts.monthEnd) as Array<{
        sid: string; local_day: string; c: number;
      }>;
    for (const r of taskRows) ensure(r.local_day, r.sid).taskCount = r.c;

    // 個人スペースのタスクは space_id が NULL（owner_id で紐付く）ため、上の
    // `space_id IN (...)` では拾えず全体カレンダーに反映されなかった。per-space カレンダーの
    // calendarTaskScope と同じ owner フォールバックで、`space_id IS NULL AND owner_id=個人スペース
    // 所有者` のタスクを個人スペース列に計上する。
    for (const ps of visibleSpaces) {
      if (ps.kind !== 'personal' || !ps.ownerId) continue;
      const personalRows = db
        .prepare(
          `SELECT date(created_at, ?) AS local_day, COUNT(*) AS c
           FROM local_tasks
           WHERE space_id IS NULL AND owner_id = ? AND date(created_at, ?) BETWEEN ? AND ?
           GROUP BY local_day`
        )
        .all(mod, ps.ownerId, mod, opts.monthStart, opts.monthEnd) as Array<{ local_day: string; c: number }>;
      for (const r of personalRows) ensure(r.local_day, ps.id).taskCount += r.c;
    }

    // 複数日対応: 当月に重なるイベントを 1 本でまとめて取得し、各日（当月内に
    // クリップ）に 1 件ずつ計上する。終了日が当月外の単日 end_date=NULL は date 自身。
    const eventRows = db
      .prepare(
        `SELECT * FROM calendar_events
         WHERE space_id IN (${placeholders})
           AND COALESCE(end_date, date) >= ? AND date <= ?
         ORDER BY date ASC, (time IS NULL) DESC, time ASC, id ASC`
      )
      .all(...ids, opts.monthStart, opts.monthEnd) as CalendarEventRow[];
    const events = eventRows.map(rowToCalendarEvent);
    for (const ev of events) {
      const start = ev.date < opts.monthStart ? opts.monthStart : ev.date;
      const end = (ev.endDate ?? ev.date) > opts.monthEnd ? opts.monthEnd : (ev.endDate ?? ev.date);
      for (const d of enumerateLocalDays(start, end)) ensure(d, ev.spaceId).eventCount += 1;
    }

    return { days, events, spaces };
  }
