/**
 * Per-(user|org, piece) grants for SSH connections.
 *
 * Design rationale (rev 4):
 *   Non-owner / non-admin access to a connection requires an explicit grant.
 *   Grants scope BOTH the subject (user or org) AND the piece (piece_name or
 *   applies_to_all_pieces=1, admin-only with reason). Optional expires_at.
 *
 *   findActiveGrant() is the hot path queried during access decisions.
 *
 * Plan: docs/superpowers/plans/2026-05-12-ssh-tool-integration.md (Phase 1).
 */
import type Database from 'better-sqlite3';
import { randomUUID } from 'node:crypto';

export type SshGrantSubjectType = 'user' | 'org';

export interface SshGrant {
  id: string;
  connectionId: string;
  subjectType: SshGrantSubjectType;
  subjectId: string;
  pieceName: string | null;
  appliesToAllPieces: boolean;
  grantedByUserId: string;
  reason: string;
  expiresAt: string | null;
  createdAt: string;
}

export interface CreateGrantInput {
  connectionId: string;
  subjectType: SshGrantSubjectType;
  subjectId: string;
  /** Required when appliesToAllPieces=false; must be null when appliesToAllPieces=true. */
  pieceName: string | null;
  appliesToAllPieces: boolean;
  grantedByUserId: string;
  reason: string;
  expiresAt?: string | null;
  /** ISO8601 timestamp; defaults to now. */
  createdAt?: string;
}

export interface FindActiveGrantArgs {
  connectionId: string;
  userId: string;
  orgIds: string[];
  pieceName: string;
  /** ISO8601 timestamp; defaults to now. */
  now?: string;
}

interface RawRow {
  id: string;
  connection_id: string;
  subject_type: SshGrantSubjectType;
  subject_id: string;
  piece_name: string | null;
  applies_to_all_pieces: number;
  granted_by_user_id: string;
  reason: string;
  expires_at: string | null;
  created_at: string;
}

function fromRow(r: RawRow): SshGrant {
  return {
    id: r.id,
    connectionId: r.connection_id,
    subjectType: r.subject_type,
    subjectId: r.subject_id,
    pieceName: r.piece_name,
    appliesToAllPieces: r.applies_to_all_pieces === 1,
    grantedByUserId: r.granted_by_user_id,
    reason: r.reason,
    expiresAt: r.expires_at,
    createdAt: r.created_at,
  };
}

export interface SshGrantsRepo {
  create(input: CreateGrantInput): SshGrant;
  delete(id: string): boolean;
  getById(id: string): SshGrant | null;
  listForConnection(connectionId: string): SshGrant[];
  /**
   * Find any grant that authorizes (userId or any of orgIds) to access
   * connectionId for pieceName, that has not expired. Returns the
   * most-specific match (piece-specific over applies_to_all_pieces,
   * then user over org).
   */
  findActiveGrant(args: FindActiveGrantArgs): SshGrant | null;
}

export function createGrantsRepo(db: Database.Database): SshGrantsRepo {
  const insertStmt = db.prepare(`
    INSERT INTO ssh_connection_grants (
      id, connection_id, subject_type, subject_id, piece_name,
      applies_to_all_pieces, granted_by_user_id, reason, expires_at, created_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);

  const deleteStmt = db.prepare(`DELETE FROM ssh_connection_grants WHERE id = ?`);

  const selectByIdStmt = db.prepare(`SELECT * FROM ssh_connection_grants WHERE id = ?`);

  const selectByConnectionStmt = db.prepare(
    `SELECT * FROM ssh_connection_grants WHERE connection_id = ? ORDER BY created_at DESC`,
  );

  return {
    create(input) {
      if (input.appliesToAllPieces && input.pieceName !== null) {
        throw new Error('grants: pieceName must be null when appliesToAllPieces=true');
      }
      if (!input.appliesToAllPieces && (input.pieceName === null || input.pieceName === '')) {
        throw new Error('grants: pieceName is required when appliesToAllPieces=false');
      }
      if (input.reason.length < 8) {
        throw new Error('grants: reason must be at least 8 characters');
      }
      if (input.subjectType !== 'user' && input.subjectType !== 'org') {
        throw new Error(`grants: invalid subjectType "${input.subjectType}"`);
      }
      const id = randomUUID();
      const createdAt = input.createdAt ?? new Date().toISOString();
      insertStmt.run(
        id,
        input.connectionId,
        input.subjectType,
        input.subjectId,
        input.pieceName,
        input.appliesToAllPieces ? 1 : 0,
        input.grantedByUserId,
        input.reason,
        input.expiresAt ?? null,
        createdAt,
      );
      return {
        id,
        connectionId: input.connectionId,
        subjectType: input.subjectType,
        subjectId: input.subjectId,
        pieceName: input.pieceName,
        appliesToAllPieces: input.appliesToAllPieces,
        grantedByUserId: input.grantedByUserId,
        reason: input.reason,
        expiresAt: input.expiresAt ?? null,
        createdAt,
      };
    },

    delete(id) {
      const r = deleteStmt.run(id);
      return r.changes > 0;
    },

    getById(id) {
      const r = selectByIdStmt.get(id) as RawRow | undefined;
      return r ? fromRow(r) : null;
    },

    listForConnection(connectionId) {
      return (selectByConnectionStmt.all(connectionId) as RawRow[]).map(fromRow);
    },

    findActiveGrant(args) {
      const now = args.now ?? new Date().toISOString();
      // Build placeholders for orgIds — better-sqlite3 doesn't bind arrays.
      const orgPlaceholders = args.orgIds.length > 0 ? args.orgIds.map(() => '?').join(',') : null;
      // Match: subject (user OR org-in-list)
      //   AND piece (specific OR all_pieces)
      //   AND not expired (expires_at IS NULL OR expires_at > now)
      // Sort priority: specific piece first (applies_to_all_pieces ASC),
      // then user subject first (subject_type ASC; 'org' < 'user' lexically,
      // so use CASE).
      const sql = `
        SELECT * FROM ssh_connection_grants
        WHERE connection_id = ?
          AND (
            (subject_type = 'user' AND subject_id = ?)
            ${orgPlaceholders ? `OR (subject_type = 'org' AND subject_id IN (${orgPlaceholders}))` : ''}
          )
          AND (applies_to_all_pieces = 1 OR piece_name = ?)
          AND (expires_at IS NULL OR expires_at > ?)
        ORDER BY applies_to_all_pieces ASC,
                 CASE subject_type WHEN 'user' THEN 0 ELSE 1 END ASC,
                 created_at DESC
        LIMIT 1
      `;
      const params: unknown[] = [args.connectionId, args.userId];
      if (orgPlaceholders) params.push(...args.orgIds);
      params.push(args.pieceName, now);
      const r = db.prepare(sql).get(...params) as RawRow | undefined;
      return r ? fromRow(r) : null;
    },
  };
}
