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

export interface SpaceRow {
  id: string;
  kind: string;
  title: string;
  description: string;
  owner_id: string | null;
  visibility: string;
  visibility_scope_org_id: string | null;
  status: string;
  brand_color: string | null;
  workspace_dir: string | null;
  created_at: string;
  updated_at: string;
}


export interface Space {
  id: string;
  kind: 'personal' | 'case';
  title: string;
  description: string;
  ownerId: string | null;
  visibility: 'private' | 'org' | 'public';
  visibilityScopeOrgId: string | null;
  status: 'open' | 'archived';
  brandColor: string | null;
  workspaceDir: string | null;
  createdAt: string;
  updatedAt: string;
}


export type SpaceMemberRoleValue = 'owner' | 'editor' | 'viewer';


/** space_members の 1 行 + users から合成した表示情報。owner_id は別途 owner として扱う。 */
export interface SpaceMember {
  spaceId: string;
  userId: string;
  role: SpaceMemberRoleValue;
  invitedBy: string | null;
  createdAt: string;
  /** users JOIN 由来の表示用フィールド（行が無い場合 null）。 */
  name: string | null;
  email: string | null;
  avatarUrl: string | null;
}


/** 招待リンクで付与できるロール。owner は付与不可（権限昇格防止）。 */
export type SpaceInviteRole = 'editor' | 'viewer';


/** space_invites の 1 行。 */
export interface SpaceInvite {
  token: string;
  spaceId: string;
  role: SpaceInviteRole;
  createdBy: string | null;
  createdAt: string;
  /** ISO 文字列。null = 無期限。 */
  expiresAt: string | null;
  /** ISO 文字列。null = 有効。 */
  revokedAt: string | null;
}


export interface CreateSpaceParams {
  id?: string;
  kind: 'personal' | 'case';
  title: string;
  description?: string;
  ownerId: string | null;
  visibility?: 'private' | 'org' | 'public';
  visibilityScopeOrgId?: string | null;
  brandColor?: string | null;
  workspaceDir?: string | null;
}


export function rowToSpace(row: SpaceRow): Space {
  return {
    id: row.id,
    kind: row.kind as Space['kind'],
    title: row.title,
    description: row.description,
    ownerId: row.owner_id,
    visibility: row.visibility as Space['visibility'],
    visibilityScopeOrgId: row.visibility_scope_org_id,
    status: row.status as Space['status'],
    brandColor: row.brand_color,
    workspaceDir: row.workspace_dir,
    createdAt: utc(row.created_at),
    updatedAt: utc(row.updated_at),
  };
}


  export async function createSpace(db: Database.Database, params: CreateSpaceParams): Promise<Space> {
    const id = params.id ?? randomUUID();
    db
      .prepare(
        `INSERT INTO spaces (id, kind, title, description, owner_id, visibility, visibility_scope_org_id, status, brand_color, workspace_dir)
         VALUES (@id, @kind, @title, @description, @ownerId, @visibility, @visibilityScopeOrgId, 'open', @brandColor, @workspaceDir)`
      )
      .run({
        id,
        kind: params.kind,
        title: params.title,
        description: params.description ?? '',
        ownerId: params.ownerId,
        visibility: params.visibility ?? 'private',
        visibilityScopeOrgId: params.visibilityScopeOrgId ?? null,
        brandColor: params.brandColor ?? null,
        workspaceDir: params.workspaceDir ?? null,
      });
    const space = await getSpace(db, id);
    if (!space) throw new Error('createSpace: failed to load inserted space');
    return space;
  }


  export async function getSpace(db: Database.Database, id: string, opts?: { viewer?: Express.User }): Promise<Space | null> {
    // 個人スペースは所有者のみ可視（admin 含む）。buildSpaceVisibilityWhere が
    // kind='personal' を owner_id 一致に限定し、kind='case' は従来通り扱う。
    const viewerClause = opts?.viewer
      ? buildSpaceVisibilityWhere(opts.viewer, 's')
      : { clause: '1=1', params: [] as unknown[] };
    const row = db
      .prepare(`SELECT s.* FROM spaces s WHERE s.id = ? AND ${viewerClause.clause}`)
      .get(id, ...viewerClause.params) as SpaceRow | undefined;
    return row ? rowToSpace(row) : null;
  }


  export async function listSpaces(db: Database.Database, opts?: { viewer?: Express.User; includeArchived?: boolean }): Promise<Space[]> {
    // 個人スペースは所有者のみ可視（admin 含む）。これにより admin の一覧/レールに
    // 他ユーザーの個人スペースが混ざらず、UI の誤ワークスペース解決を防ぐ。
    const viewerClause = opts?.viewer
      ? buildSpaceVisibilityWhere(opts.viewer, 's')
      : { clause: '1=1', params: [] as unknown[] };
    const statusClause = opts?.includeArchived ? '1=1' : "s.status = 'open'";
    const rows = db
      .prepare(`
        SELECT s.* FROM spaces s
        WHERE ${statusClause} AND ${viewerClause.clause}
        ORDER BY s.kind = 'personal' DESC, s.updated_at DESC
      `)
      .all(...viewerClause.params) as SpaceRow[];
    return rows.map(rowToSpace);
  }


  export async function updateSpace(db: Database.Database, id: string, patch: { title?: string; description?: string; brandColor?: string | null; visibility?: 'private' | 'org' | 'public'; visibilityScopeOrgId?: string | null }): Promise<Space | null> {
    const sets: string[] = [];
    const vals: unknown[] = [];
    if (patch.title !== undefined) { sets.push('title = ?'); vals.push(patch.title); }
    if (patch.description !== undefined) { sets.push('description = ?'); vals.push(patch.description); }
    if (patch.brandColor !== undefined) { sets.push('brand_color = ?'); vals.push(patch.brandColor); }
    if (patch.visibility !== undefined) { sets.push('visibility = ?'); vals.push(patch.visibility); }
    if (patch.visibilityScopeOrgId !== undefined) { sets.push('visibility_scope_org_id = ?'); vals.push(patch.visibilityScopeOrgId); }
    if (sets.length === 0) return getSpace(db, id);
    sets.push("updated_at = datetime('now')");
    db.prepare(`UPDATE spaces SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
    return getSpace(db, id);
  }


  export async function archiveSpace(db: Database.Database, id: string): Promise<void> {
    db
      .prepare(`UPDATE spaces SET status = 'archived', updated_at = datetime('now') WHERE id = ? AND kind = 'case'`)
      .run(id);
  }


  export async function setSpaceWorkspaceDir(db: Database.Database, id: string, workspaceDir: string): Promise<void> {
    db
      .prepare(`UPDATE spaces SET workspace_dir = ?, updated_at = datetime('now') WHERE id = ?`)
      .run(workspaceDir, id);
  }


  /**
   * ロールバック専用のハード削除。通常は archiveSpace を使う。
   * 個人スペースは「削除不可」(spec §5.7) なので kind='case' のみ消す。
   * この backstop により、誤って personal id を渡しても個人スペースは保護される。
   */
  export async function hardDeleteSpace(db: Database.Database, id: string): Promise<void> {
    const tx = db.transaction((sid: string) => {
      // Crypto-shred space-owned credential DEKs EXPLICITLY (Space-as-Principal P2).
      // We don't rely on FK ON DELETE CASCADE because migrate.ts intentionally
      // omits FK clauses, so a migrated DB's space_ssh_deks would otherwise
      // outlive the space. Gate on kind='case' (subquery) so a mistaken personal
      // id never shreds a personal space's DEK. Runs before the spaces delete so
      // the guard subquery still sees the row.
      const isCase = `AND space_id IN (SELECT id FROM spaces WHERE kind = 'case')`;
      db.prepare(`DELETE FROM space_ssh_deks WHERE space_id = ? ${isCase}`).run(sid);
      db.prepare(`DELETE FROM space_browser_deks WHERE space_id = ? ${isCase}`).run(sid);
      db.prepare(`DELETE FROM spaces WHERE id = ? AND kind = 'case'`).run(sid);
    });
    tx(id);
  }


  /**
   * ユーザーの個人スペースを返す。無ければ生成する（冪等）。
   * UNIQUE INDEX idx_spaces_personal_owner により1ユーザー1個に保たれる。
   * 既存ユーザーの初回アクセス移行を兼ねる（spec §7）。
   * 個人スペースの workspace_dir はこの計画では NULL（フォルダ物理ワイヤリングは計画2）。
   */
  export async function ensurePersonalSpace(db: Database.Database, ownerId: string, displayName?: string): Promise<Space> {
    const existing = db
      .prepare(`SELECT s.* FROM spaces s WHERE s.owner_id = ? AND s.kind = 'personal'`)
      .get(ownerId) as SpaceRow | undefined;
    if (existing) return rowToSpace(existing);
    return createSpace(db, {
      kind: 'personal',
      title: displayName ? `${displayName} の個人ワークスペース` : '個人ワークスペース',
      ownerId,
      visibility: 'private',
    });
  }


  // ─── スペース・メンバー（協働者）CRUD ─────────────────────────────
  // owner_id は members 行を持たない根オーナー。これらは追加協働者だけを扱う。

  /**
   * メンバーを追加または上書きする（冪等）。既存行があれば role/invited_by を更新。
   * owner_id 本人を渡してはならない（既に owner）。呼び出し側でガードする。
   */
  export async function addSpaceMember(db: Database.Database, params: {
    spaceId: string;
    userId: string;
    role: SpaceMemberRoleValue;
    invitedBy?: string | null;
  }): Promise<void> {
    db
      .prepare(
        `INSERT INTO space_members (space_id, user_id, role, invited_by)
         VALUES (@spaceId, @userId, @role, @invitedBy)
         ON CONFLICT(space_id, user_id) DO UPDATE SET
           role = excluded.role,
           invited_by = excluded.invited_by`
      )
      .run({
        spaceId: params.spaceId,
        userId: params.userId,
        role: params.role,
        invitedBy: params.invitedBy ?? null,
      });
  }


  /** スペースのメンバー一覧（users JOIN で表示情報付き）。owner_id は含まない。 */
  export async function listSpaceMembers(db: Database.Database, spaceId: string): Promise<SpaceMember[]> {
    const rows = db
      .prepare(`
        SELECT sm.space_id, sm.user_id, sm.role, sm.invited_by, sm.created_at,
               u.name AS name, u.email AS email, u.avatar_url AS avatar_url
        FROM space_members sm
        LEFT JOIN users u ON u.id = sm.user_id
        WHERE sm.space_id = ?
        ORDER BY sm.created_at ASC, sm.user_id ASC
      `)
      .all(spaceId) as Array<{
        space_id: string;
        user_id: string;
        role: string;
        invited_by: string | null;
        created_at: string;
        name: string | null;
        email: string | null;
        avatar_url: string | null;
      }>;
    return rows.map((r) => ({
      spaceId: r.space_id,
      userId: r.user_id,
      role: r.role as SpaceMemberRoleValue,
      invitedBy: r.invited_by,
      createdAt: r.created_at,
      name: r.name,
      email: r.email,
      avatarUrl: r.avatar_url,
    }));
  }


  /**
   * 指定ユーザーのメンバーロールを返す。メンバー行が無ければ null。
   * 根オーナー（owner_id）は members 行を持たないため null を返す。
   * 呼び出し側は owner_id を別途 'owner' として扱うこと（canManageSpace 参照）。
   */
  export function getSpaceMemberRole(db: Database.Database, spaceId: string, userId: string): SpaceMemberRoleValue | null {
    const row = db
      .prepare(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`)
      .get(spaceId, userId) as { role: string } | undefined;
    return row ? (row.role as SpaceMemberRoleValue) : null;
  }


  /** スペースのツール制限ポリシー（JSON 文字列）を返す。未設定なら null。 */
  export function getSpaceToolPolicy(db: Database.Database, spaceId: string): string | null {
    const row = db
      .prepare(`SELECT tool_policy FROM spaces WHERE id = ?`)
      .get(spaceId) as { tool_policy: string | null } | undefined;
    return row?.tool_policy ?? null;
  }


  /** スペースのツール制限ポリシーを更新する。null を渡すとクリア。 */
  export function setSpaceToolPolicy(db: Database.Database, spaceId: string, policyJson: string | null): void {
    db
      .prepare(`UPDATE spaces SET tool_policy = ?, updated_at = datetime('now') WHERE id = ?`)
      .run(policyJson, spaceId);
  }


  /** スペースの Python パッケージ desired-state（JSON 文字列）を返す。未設定なら null。 */
  export function getSpacePythonPackages(db: Database.Database, spaceId: string): string | null {
    const row = db
      .prepare(`SELECT python_packages FROM spaces WHERE id = ?`)
      .get(spaceId) as { python_packages: string | null } | undefined;
    return row?.python_packages ?? null;
  }


  /** スペースの Python パッケージ desired-state を更新する。null を渡すとクリア。 */
  export function setSpacePythonPackages(db: Database.Database, spaceId: string, packagesJson: string | null): void {
    db
      .prepare(`UPDATE spaces SET python_packages = ?, updated_at = datetime('now') WHERE id = ?`)
      .run(packagesJson, spaceId);
  }


  /**
   * 指定 user がスペースの行（タスク/ファイル/ログ等）を VIEW できるか。
   * buildVisibilityWhere の membership OR ブランチと同じルールをコード上で再現する：
   *   admin → true / space.owner_id === user.id（根オーナー）→ true /
   *   space_members に (spaceId, user.id) 行あり → true / それ以外 → false。
   * 二次ゲート（canViewTask / canUserSeeTask）が呼び出す。書き込み判定ではない。
   */
  export function userCanViewSpace(db: Database.Database, spaceId: string, user: { id: string; role: string }): boolean {
    if (user.role === 'admin') return true;
    const owns = db
      .prepare(`SELECT 1 FROM spaces WHERE id = ? AND owner_id = ?`)
      .get(spaceId, user.id);
    if (owns) return true;
    const member = db
      .prepare(`SELECT 1 FROM space_members WHERE space_id = ? AND user_id = ?`)
      .get(spaceId, user.id);
    return !!member;
  }


  /** 既存メンバーのロールを変更する。行が無ければ no-op。 */
  export async function updateSpaceMemberRole(db: Database.Database, spaceId: string, userId: string, role: SpaceMemberRoleValue): Promise<void> {
    db
      .prepare(`UPDATE space_members SET role = ? WHERE space_id = ? AND user_id = ?`)
      .run(role, spaceId, userId);
  }


  /** メンバーを除去する。行が無ければ no-op。 */
  export async function removeSpaceMember(db: Database.Database, spaceId: string, userId: string): Promise<void> {
    db
      .prepare(`DELETE FROM space_members WHERE space_id = ? AND user_id = ?`)
      .run(spaceId, userId);
  }


  // ─── スペース招待リンク（再利用トークン）─────────────────────────────

  export function rowToSpaceInvite(row: {
    token: string;
    space_id: string;
    role: string;
    created_by: string | null;
    created_at: string;
    expires_at: string | null;
    revoked_at: string | null;
  }): SpaceInvite {
    return {
      token: row.token,
      spaceId: row.space_id,
      role: row.role as SpaceInviteRole,
      createdBy: row.created_by,
      createdAt: row.created_at,
      expiresAt: row.expires_at,
      revokedAt: row.revoked_at,
    };
  }


  /**
   * 招待が今この瞬間に有効か（無効化されておらず、期限切れでない）。
   * 期限比較は SQLite と同じ datetime('now')（UTC）基準の ISO 文字列辞書順。
   */
  export function isSpaceInviteValid(db: Database.Database, invite: SpaceInvite | null | undefined): invite is SpaceInvite {
    if (!invite) return false;
    if (invite.revokedAt) return false;
    if (invite.expiresAt) {
      const now = db.prepare(`SELECT datetime('now') AS n`).get() as { n: string };
      if (invite.expiresAt <= now.n) return false;
    }
    return true;
  }


  /**
   * スペースの招待リンクを (再)生成する。再利用方針＝スペースごとに有効リンク1本。
   * 既存 invite 行は削除してから新トークンを 1 行 insert する。
   * @param expiresInDays 有効日数。未指定/null は無期限。期限は SQLite の
   *   datetime('now','+N days') で保存し、isSpaceInviteValid と同じ形式に揃える。
   */
  export function createSpaceInvite(db: Database.Database, params: {
    spaceId: string;
    role: SpaceInviteRole;
    createdBy?: string | null;
    expiresInDays?: number | null;
  }): SpaceInvite {
    const token = randomUUID();
    // 整数日数のみ受理（負/0/非整数は無期限扱い）。文字列補間するため厳格に検証する。
    const days =
      typeof params.expiresInDays === 'number' &&
      Number.isInteger(params.expiresInDays) &&
      params.expiresInDays > 0
        ? params.expiresInDays
        : null;
    const tx = db.transaction(() => {
      db.prepare(`DELETE FROM space_invites WHERE space_id = ?`).run(params.spaceId);
      db
        .prepare(
          `INSERT INTO space_invites (token, space_id, role, created_by, expires_at)
           VALUES (@token, @spaceId, @role, @createdBy,
                   ${days === null ? 'NULL' : `datetime('now', '+${days} days')`})`,
        )
        .run({
          token,
          spaceId: params.spaceId,
          role: params.role,
          createdBy: params.createdBy ?? null,
        });
    });
    tx();
    return getSpaceInviteByToken(db, token)!;
  }


  /** スペースの現行 invite（有効・無効問わず最新の1本）。無ければ null。 */
  export function getSpaceInvite(db: Database.Database, spaceId: string): SpaceInvite | null {
    const row = db
      .prepare(`SELECT * FROM space_invites WHERE space_id = ? ORDER BY created_at DESC LIMIT 1`)
      .get(spaceId) as Parameters<typeof rowToSpaceInvite>[0] | undefined;
    return row ? rowToSpaceInvite(row) : null;
  }


  /** トークンで invite を引く（有効性は呼び出し側で判定）。無ければ null。 */
  export function getSpaceInviteByToken(db: Database.Database, token: string): SpaceInvite | null {
    const row = db
      .prepare(`SELECT * FROM space_invites WHERE token = ?`)
      .get(token) as Parameters<typeof rowToSpaceInvite>[0] | undefined;
    return row ? rowToSpaceInvite(row) : null;
  }


  /** スペースの現行 invite を無効化する。行が無ければ no-op。 */
  export function revokeSpaceInvite(db: Database.Database, spaceId: string): void {
    db
      .prepare(
        `UPDATE space_invites SET revoked_at = datetime('now')
         WHERE space_id = ? AND revoked_at IS NULL`,
      )
      .run(spaceId);
  }


  export function getSpaceA2aSkills(db: Database.Database, spaceId: string): string[] {
    const r = db.prepare('SELECT a2a_skills FROM spaces WHERE id = ?').get(spaceId) as { a2a_skills: string | null } | undefined;
    if (!r || !r.a2a_skills) return [];
    try { const v = JSON.parse(r.a2a_skills); return Array.isArray(v) ? v : []; } catch { return []; }
  }


  export function setSpaceA2aSkills(db: Database.Database, spaceId: string, skills: string[]): void {
    db.prepare('UPDATE spaces SET a2a_skills = ? WHERE id = ?').run(JSON.stringify(skills), spaceId);
  }
