/**
 * lib/authz.js — Pure authorization layer.
 *
 * The can* helpers are pure functions: they take DB rows as input and return
 * booleans. No DB calls inside can*. Use these for conditional rendering or
 * early-exit checks.
 *
 * requireRole() throws on insufficient privilege.
 * assertArtifactAccess() is a stub that reads artifact + ACL and evaluates
 * via can* — Tasks #5/#6/#7 wire sensitive paths through this.
 *
 * Role hierarchy: owner > admin > member > viewer
 */

import { getDb, nowIso, newId } from './db.js';

// ── Role hierarchy ────────────────────────────────────────────────────────────

const ROLE_RANK = { owner: 4, admin: 3, member: 2, viewer: 1 };

function _rankOf(role) {
  return ROLE_RANK[role] ?? 0;
}

function _hasRoleAtLeast(sessionRole, minimumRole) {
  return _rankOf(sessionRole) >= _rankOf(minimumRole);
}

// ── Access matrix helpers ─────────────────────────────────────────────────────

/**
 * Returns true if the session is the artifact owner.
 */
function _isOwner(artifact, session) {
  return artifact?.owner_user_id && artifact.owner_user_id === session?.user_id;
}

/**
 * Returns true if session org matches artifact's project org (or artifact has
 * no org context — legacy artifacts before Phase 1 migration).
 */
function _sameOrg(artifact, session) {
  // Phase 1: all artifacts belong to 'personal-org' implicitly.
  // Phase 2+: artifact will carry org_id via project join.
  // For now, org match is verified at the API layer before calling can*.
  return true;
}

// ── can* helpers (pure, no DB calls) ─────────────────────────────────────────

/**
 * Can the session read this artifact?
 *
 * | scope   | viewer | member | admin | owner |
 * |---------|--------|--------|-------|-------|
 * | private | own    | own    | yes   | yes   |
 * | org     | yes    | yes    | yes   | yes   |
 * | team    | if in team (checked at ACL level) | same | yes | yes |
 * | link    | (share link path — not checked here) |
 *
 * The 'team' scope and 'link' scope require ACL-row context not available to
 * this pure helper. Those checks are handled in assertArtifactAccess().
 * This helper conservatively returns false for 'team'/'link' — callers
 * should prefer assertArtifactAccess() for those scopes.
 */
export function canRead(artifact, session) {
  if (!artifact || !session) return false;
  const scope = artifact.sharing_scope || 'private';
  const role = session.role;

  if (scope === 'org') return true; // all org members can read
  if (scope === 'private') {
    if (_hasRoleAtLeast(role, 'admin')) return true; // admins/owners see all private
    return _isOwner(artifact, session);              // members/viewers see own only
  }
  // 'team' and 'link' — require ACL context; conservatively deny here
  return _hasRoleAtLeast(role, 'admin');
}

/**
 * Can the session write (update) this artifact?
 * Admins and owners can write any artifact. Members can write their own.
 * Viewers cannot write.
 */
export function canWrite(artifact, session) {
  if (!artifact || !session) return false;
  const role = session.role;
  if (_hasRoleAtLeast(role, 'admin')) return true;
  if (role === 'member' && _isOwner(artifact, session)) return true;
  return false;
}

/**
 * Can the session create a share link for this artifact?
 * member+ can create share links.
 */
export function canShare(artifact, session) {
  if (!artifact || !session) return false;
  if (!_hasRoleAtLeast(session.role, 'member')) return false;
  return canRead(artifact, session);
}

/**
 * Can the session delete this artifact?
 * Admins/owners can delete any. Members can delete their own. Viewers cannot.
 */
export function canDelete(artifact, session) {
  if (!artifact || !session) return false;
  const role = session.role;
  if (_hasRoleAtLeast(role, 'admin')) return true;
  if (role === 'member' && _isOwner(artifact, session)) return true;
  return false;
}

// ── requireRole ───────────────────────────────────────────────────────────────

/**
 * Throws { status: 403, error: 'insufficient_role' } if the session's role
 * is below minimumRole.
 *
 * @param {object} session
 * @param {'owner'|'admin'|'member'|'viewer'} minimumRole
 * @param {string} [orgId] — if provided, verifies session.org_id matches
 */
export function requireRole(session, minimumRole, orgId) {
  if (!session) throw { status: 403, error: 'insufficient_role' };
  if (orgId && session.org_id !== orgId) throw { status: 403, error: 'insufficient_role' };
  if (!_hasRoleAtLeast(session.role, minimumRole)) {
    throw { status: 403, error: 'insufficient_role' };
  }
}

// ── assertArtifactAccess (stub, Phase 1) ──────────────────────────────────────

/**
 * Reads artifact + ACL from DB, evaluates via can*, writes an audit row for
 * every allow AND deny call, and throws 403 on denial.
 *
 * This is the Phase 1 stub. Tasks #5/#6/#7 will wire sensitive paths through
 * this function and extend the share/team scope logic.
 *
 * @param {string} artifactId
 * @param {object} session
 * @param {'read'|'write'|'share'|'delete'|'export'} action
 * @param {object} [req] — for IP / user-agent capture
 */
export async function assertArtifactAccess(artifactId, session, action, req) {
  const db = getDb();

  // Read artifact row.
  const artifact = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(artifactId);
  if (!artifact) {
    // Artifact not found — log as denied and throw 404-flavoured 403.
    _writeAuditRow(db, {
      session,
      action: `artifact.${action}.denied`,
      artifactId,
      status: 'denied',
      summary: `Artifact ${artifactId} not found`,
      req,
    });
    throw { status: 403, error: 'artifact_not_found_or_denied' };
  }

  // Read ACL row (may be absent for legacy artifacts — default to artifact.sharing_scope).
  const acl = db.prepare('SELECT * FROM artifact_acls WHERE artifact_id = ?').get(artifactId);
  const effectiveScope = acl?.scope || artifact.sharing_scope || 'private';
  const artifactWithScope = { ...artifact, sharing_scope: effectiveScope };

  // Evaluate.
  let allowed = false;
  switch (action) {
    case 'read':   allowed = canRead(artifactWithScope, session); break;
    case 'write':  allowed = canWrite(artifactWithScope, session); break;
    case 'share':  allowed = canShare(artifactWithScope, session); break;
    case 'delete': allowed = canDelete(artifactWithScope, session); break;
    case 'export': allowed = canRead(artifactWithScope, session); break; // export = read + download
    default:       allowed = false;
  }

  const auditAction = allowed ? `artifact.${action}` : `artifact.${action}.denied`;
  _writeAuditRow(db, {
    session,
    action: auditAction,
    artifactId,
    status: allowed ? 'success' : 'denied',
    summary: `${action} on artifact ${artifactId}: ${allowed ? 'allowed' : 'denied'}`,
    req,
  });

  if (!allowed) {
    throw { status: 403, error: 'access_denied' };
  }

  return artifact;
}

// ── getMembership (convenience re-export for callers who don't import org.js) ─

/**
 * Returns membership row for userId in orgId, or null.
 * Synchronous DB read — safe to call from pure auth checks.
 */
export function getMembership(userId, orgId) {
  if (!userId || !orgId) return null;
  return (
    getDb()
      .prepare('SELECT * FROM org_memberships WHERE user_id = ? AND org_id = ?')
      .get(userId, orgId) || null
  );
}

// ── Internal ─────────────────────────────────────────────────────────────────

function _writeAuditRow(db, { session, action, artifactId, status, summary, req }) {
  try {
    const ip = _requestIp(req);
    const ua = req?.headers?.get?.('user-agent') || req?.headers?.['user-agent'] || null;
    db.prepare(
      `INSERT INTO audit_logs
         (id, at, type, summary, status, meta, user_id, org_id, ip, user_agent,
          action_target_id, action_target_type)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
    ).run(
      newId('audit'),
      nowIso(),
      action,
      summary,
      status,
      JSON.stringify({ artifact_id: artifactId }),
      session?.user_id || null,
      session?.org_id || null,
      ip,
      ua,
      artifactId,
      'artifact'
    );
  } catch (err) {
    console.error('[authz] Failed to write audit row:', err.message);
  }
}

function _requestIp(req) {
  const xff = req?.headers?.get?.('x-forwarded-for') || req?.headers?.['x-forwarded-for'];
  if (xff) return String(xff).split(',')[0].trim();
  return null;
}
