/**
 * lib/artifact-acl.js — Artifact ACL domain.
 *
 * Provides the full lifecycle for:
 *   - ACL scope management (private / team / org / link)
 *   - Signed share links (HMAC-SHA256 token, expiry, view cap, revoke)
 *   - Optional email-OTP gate per share link / per org toggle
 *   - Compliance audit writes on every material action
 *
 * Token design (RD-3):
 *   1. Generate 32 random bytes, base64url-encode → visible token for URL.
 *   2. HMAC-SHA256(token, MADARX_SHARE_LINK_SECRET) → store hex digest as token_hmac.
 *   3. URL: /share/<token>
 *   4. Verification: recompute HMAC, compare with timingSafeEqual.
 *
 * Secret management:
 *   MADARX_SHARE_LINK_SECRET is read from the environment. If absent at first
 *   use, this module falls back to the secrets vault. If still absent, it
 *   generates a random key, persists it to the vault with a console warning,
 *   and uses it for the lifetime of the process.
 */

import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
import { getDb, nowIso, newId } from './db.js';
import {
  appendComplianceAudit,
  ComplianceActions,
  requestIp,
  requestUserAgent,
} from './compliance-audit.js';
import {
  resolveShareSecret as _resolveShareSecret,
  computeHmac as _computeHmac,
  hmacMatch as _hmacMatch,
} from './share-secret.js';
import { publicShareUrl } from './share-url.js';

// Re-exported so existing importers (`import { checkIpRateLimit } from
// '@/lib/artifact-acl.js'`) keep working after the helper extraction.
export { checkIpRateLimit } from './share-secret.js';

// ── getAcl ────────────────────────────────────────────────────────────────────

/**
 * Returns the ACL row for an artifact, or a synthetic default if absent.
 *
 * @param {string} artifactId
 * @returns {{ artifact_id: string, scope: string, team_id: string|null, updated_at: string, updated_by: string|null }}
 */
export function getAcl(artifactId) {
  const db = getDb();
  const row = db.prepare('SELECT * FROM artifact_acls WHERE artifact_id = ?').get(artifactId);
  if (row) return row;
  // Synthetic default — scope is 'private' until explicitly set.
  return {
    artifact_id: artifactId,
    scope: 'private',
    team_id: null,
    updated_at: nowIso(),
    updated_by: null,
  };
}

// ── setAcl ────────────────────────────────────────────────────────────────────

/**
 * Sets the ACL scope for an artifact.
 *
 * @param {string} artifactId
 * @param {{ scope: string, team_id?: string, updated_by: string }} opts
 * @returns {{ artifact_id: string, scope: string }}
 */
export function setAcl(artifactId, { scope, team_id = null, updated_by }) {
  const validScopes = ['private', 'team', 'org', 'link'];
  if (!validScopes.includes(scope)) {
    throw { status: 400, error: 'invalid_scope', validScopes };
  }
  if (scope === 'team' && !team_id) {
    throw { status: 400, error: 'team_id_required_for_team_scope' };
  }

  const db = getDb();

  // Read existing scope to detect 'link' → other transitions.
  const existing = db.prepare('SELECT scope FROM artifact_acls WHERE artifact_id = ?').get(artifactId);

  // If changing FROM 'link' to anything else, revoke all active share links.
  if (existing?.scope === 'link' && scope !== 'link') {
    db.prepare(
      `UPDATE artifact_share_links
         SET revoked = 1
       WHERE artifact_id = ? AND revoked = 0`
    ).run(artifactId);
  }

  db.prepare(
    `INSERT INTO artifact_acls (artifact_id, scope, team_id, updated_at, updated_by)
     VALUES (?, ?, ?, ?, ?)
     ON CONFLICT(artifact_id) DO UPDATE SET
       scope      = excluded.scope,
       team_id    = excluded.team_id,
       updated_at = excluded.updated_at,
       updated_by = excluded.updated_by`
  ).run(artifactId, scope, team_id, nowIso(), updated_by || null);

  return { artifact_id: artifactId, scope, team_id };
}

// ── createShareLink ───────────────────────────────────────────────────────────

/**
 * Creates a signed share link for an artifact.
 *
 * @param {string} artifactId
 * @param {{ expires_in_days?: number, max_views?: number, require_email_otp?: boolean }} opts
 * @param {object} session
 * @returns {Promise<{ id: string, url: string, expires_at: string|null, max_views: number, require_email_otp: boolean }>}
 */
export async function createShareLink(artifactId, opts = {}, session) {
  const {
    expires_in_days = 7,
    max_views = 50,
    require_email_otp,
  } = opts;

  // Validate expiry cap.
  if (expires_in_days != null && expires_in_days > 90) {
    throw { status: 400, error: 'expires_in_days_exceeds_maximum', max: 90 };
  }

  const db = getDb();

  // Determine require_email_otp: explicit opt wins, otherwise read org setting.
  let emailOtp = require_email_otp;
  if (emailOtp === undefined || emailOtp === null) {
    const org = db.prepare('SELECT require_share_email_verification FROM organizations WHERE id = ?').get(session.org_id);
    emailOtp = org?.require_share_email_verification === 1;
  }

  // Generate token: 32 random bytes as base64url.
  const tokenBytes = randomBytes(32);
  const token = tokenBytes.toString('base64url');

  // Compute HMAC and store only the digest.
  const secret = await _resolveShareSecret();
  const tokenHmac = _computeHmac(token, secret);

  // Compute expires_at.
  let expiresAt = null;
  if (expires_in_days != null && expires_in_days > 0) {
    const d = new Date();
    d.setDate(d.getDate() + expires_in_days);
    expiresAt = d.toISOString();
  }

  const id = newId('share');
  const orgId = session.org_id;

  db.prepare(
    `INSERT INTO artifact_share_links
       (id, artifact_id, token, token_hmac, created_by, org_id,
        expires_at, max_views, access_count, revoked, require_email_otp, created_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`
  ).run(
    id,
    artifactId,
    token,
    tokenHmac,
    session.user_id,
    orgId,
    expiresAt,
    max_views ?? null,
    emailOtp ? 1 : 0,
    nowIso()
  );

  // Compliance audit.
  try {
    appendComplianceAudit({
      user_id: session.user_id,
      org_id: orgId,
      action: ComplianceActions.ARTIFACT_SHARE_LINK_CREATED,
      action_target_id: artifactId,
      action_target_type: 'artifact',
      status: 'success',
      summary: `Share link created for artifact ${artifactId}`,
      meta: {
        share_link_id: id,
        expires_at: expiresAt,
        max_views: max_views ?? null,
        require_email_otp: emailOtp,
      },
    });
  } catch (auditErr) {
    console.error('[artifact-acl] Failed to write audit row for createShareLink:', auditErr.message);
  }

  return {
    id,
    token,
    url: publicShareUrl(`/share/${token}`),
    expires_at: expiresAt,
    max_views: max_views ?? null,
    require_email_otp: emailOtp,
  };
}

// ── listShareLinks ────────────────────────────────────────────────────────────

/**
 * Lists active (non-revoked) share links for an artifact.
 *
 * @param {string} artifactId
 * @returns {object[]}
 */
export function listShareLinks(artifactId) {
  return getDb()
    .prepare(
      `SELECT id, artifact_id, token, created_by, org_id, expires_at, max_views,
              access_count, revoked, require_email_otp, created_at
       FROM artifact_share_links
       WHERE artifact_id = ? AND revoked = 0
       ORDER BY created_at DESC`
    )
    .all(artifactId);
}

// ── verifyShareLink ───────────────────────────────────────────────────────────

/**
 * Verifies a share token. Returns full link + artifact rows on success.
 *
 * @param {string} token  The raw base64url token from the URL
 * @param {object} [req]  Request object (for IP/UA in audit)
 * @returns {Promise<
 *   { ok: false, status: number, reason: string } |
 *   { ok: true, link: object, artifact: object }
 * >}
 */
export async function verifyShareLink(token, req) {
  if (!token) return { ok: false, status: 404, reason: 'not_found' };

  const secret = await _resolveShareSecret();
  const candidateHmac = _computeHmac(token, secret);

  const db = getDb();

  // Lookup by HMAC (never store the raw token).
  const link = db
    .prepare('SELECT * FROM artifact_share_links WHERE token_hmac = ?')
    .get(candidateHmac);

  if (!link) return { ok: false, status: 404, reason: 'not_found' };

  // Timing-safe comparison (both are hex strings of the same HMAC).
  if (!_hmacMatch(candidateHmac, link.token_hmac)) {
    return { ok: false, status: 404, reason: 'not_found' };
  }

  if (link.revoked === 1) {
    return { ok: false, status: 410, reason: 'revoked' };
  }

  if (link.expires_at && new Date(link.expires_at) < new Date()) {
    return { ok: false, status: 410, reason: 'expired' };
  }

  if (link.max_views != null && link.access_count >= link.max_views) {
    // Auto-revoke on cap reached.
    db.prepare('UPDATE artifact_share_links SET revoked = 1 WHERE id = ?').run(link.id);
    return { ok: false, status: 410, reason: 'view_cap_reached' };
  }

  const artifact = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(link.artifact_id);
  if (!artifact) return { ok: false, status: 404, reason: 'not_found' };

  return { ok: true, link, artifact };
}

// ── recordShareLinkAccess ─────────────────────────────────────────────────────

/**
 * Atomically increments access_count and writes a compliance audit row.
 * If count reaches max_views, also sets revoked = 1 in the same statement.
 *
 * @param {string} linkId
 * @param {object} req
 * @param {string|null} [recipientEmail]
 */
export function recordShareLinkAccess(linkId, req, recipientEmail = null) {
  const db = getDb();

  // Atomic increment: avoids TOCTOU race by combining the SELECT and UPDATE into
  // a single statement. Auto-revokes when access_count + 1 reaches max_views.
  // Returns 0 changes if the link was already revoked or doesn't exist (race-loss).
  const result = db.prepare(
    `UPDATE artifact_share_links
        SET access_count = access_count + 1,
            revoked      = CASE
                             WHEN max_views IS NOT NULL
                              AND access_count + 1 >= max_views THEN 1
                             ELSE revoked
                           END
      WHERE id = ?
        AND revoked = 0
        AND (max_views IS NULL OR access_count < max_views)`
  ).run(linkId);

  if (result.changes === 0) return; // race-loss or already revoked

  // Read back the updated row for the audit record.
  const link = db.prepare('SELECT * FROM artifact_share_links WHERE id = ?').get(linkId);
  if (!link) return;

  const newCount = link.access_count;
  const shouldRevoke = link.revoked === 1;

  const ip = requestIp(req);
  const ua = requestUserAgent(req);

  try {
    appendComplianceAudit({
      user_id: null,
      org_id: link.org_id,
      action: ComplianceActions.ARTIFACT_SHARE_LINK_ACCESSED,
      action_target_id: link.artifact_id,
      action_target_type: 'artifact',
      status: 'success',
      summary: `Share link ${linkId} accessed${recipientEmail ? ` by ${recipientEmail}` : ''}`,
      meta: {
        share_link_id: linkId,
        access_count: newCount,
        auto_revoked: shouldRevoke,
        recipient_email: recipientEmail,
      },
      ip,
      user_agent: ua,
    });
  } catch (auditErr) {
    console.error('[artifact-acl] Failed to write audit row for recordShareLinkAccess:', auditErr.message);
  }
}

// ── revokeShareLink ───────────────────────────────────────────────────────────

/**
 * Manually revokes a share link. Writes a compliance audit row.
 *
 * @param {string} linkId
 * @param {object} session
 */
export function revokeShareLink(linkId, session) {
  const db = getDb();
  const link = db.prepare('SELECT * FROM artifact_share_links WHERE id = ?').get(linkId);
  if (!link) throw { status: 404, error: 'share_link_not_found' };

  db.prepare('UPDATE artifact_share_links SET revoked = 1 WHERE id = ?').run(linkId);

  try {
    appendComplianceAudit({
      user_id: session?.user_id || null,
      org_id: link.org_id || session?.org_id || 'personal-org',
      action: ComplianceActions.ARTIFACT_SHARE_LINK_REVOKED,
      action_target_id: link.artifact_id,
      action_target_type: 'artifact',
      status: 'success',
      summary: `Share link ${linkId} revoked`,
      meta: { share_link_id: linkId, artifact_id: link.artifact_id },
    });
  } catch (auditErr) {
    console.error('[artifact-acl] Failed to write audit row for revokeShareLink:', auditErr.message);
  }

  return { ok: true };
}

// ── createOtpForShareLink ─────────────────────────────────────────────────────

/**
 * Generates a 6-digit OTP for the email-OTP gate, stores a hashed version,
 * and attempts to send the code via the email_send tool.
 *
 * Rate limiting: callers should check that there is no recent unexpired OTP
 * before calling (prevents OTP flood). This function does NOT enforce the
 * outer rate limit — it trusts the route handler to do so.
 *
 * @param {string} linkId
 * @param {string} email
 * @returns {Promise<{ ok: true, expires_in: number } | { ok: false, error: string }>}
 */
export async function createOtpForShareLink(linkId, email) {
  const db = getDb();
  const link = db.prepare('SELECT * FROM artifact_share_links WHERE id = ?').get(linkId);
  if (!link || link.revoked === 1) {
    return { ok: false, error: 'link_invalid' };
  }

  // Generate 6-digit OTP.
  const code = String(Math.floor(100000 + (randomBytes(3).readUIntBE(0, 3) % 900000))).padStart(6, '0');

  // Hash with SHA-256 + per-row random salt for storage.
  const salt = randomBytes(16).toString('hex');
  const codeHash = createHash('sha256').update(salt + ':' + code).digest('hex');
  const storedHash = `${salt}:${codeHash}`;

  const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString(); // 10 minutes

  const id = newId('otp');
  db.prepare(
    `INSERT INTO artifact_share_link_otps
       (id, share_link_id, email, code_hash, expires_at, attempts, created_at)
     VALUES (?, ?, ?, ?, ?, 0, ?)`
  ).run(id, linkId, email, storedHash, expiresAt, nowIso());

  // Audit OTP sent.
  try {
    appendComplianceAudit({
      user_id: null,
      org_id: link.org_id,
      action: ComplianceActions.SHARE_OTP_SENT,
      action_target_id: link.artifact_id,
      action_target_type: 'artifact',
      status: 'success',
      summary: `Share OTP sent to ${email} for link ${linkId}`,
      meta: { share_link_id: linkId, otp_id: id },
    });
  } catch (auditErr) {
    console.error('[artifact-acl] Failed to write audit row for createOtpForShareLink:', auditErr.message);
  }

  // Send OTP via email_send tool.
  // The email_send tool is irreversible_external and requires an allowlist entry
  // for the recipient domain. For the OTP path we pass owner_trusted: true so
  // the approval gate is bypassed — the allowlist and rate limit still apply.
  //
  // DEVIATION from spec: we do not add a custom 'is_share_link_otp' registry
  // flag (Task #8 scope). Instead we attempt the send with owner_trusted: true
  // and surface failure as { ok: false, error: 'email_disabled' } so the UI
  // can gracefully inform the user. The allowlist gate will block sends to
  // domains not in the tool_allowlists table.
  try {
    const { executeRegistryTool } = await import('./tools/registry.js');
    const emailResult = await executeRegistryTool(
      'email_send',
      {
        to: email,
        subject: 'Your madarX share link verification code',
        html: [
          '<p>You requested access to a shared artifact on madarX.</p>',
          `<p><strong>Your verification code is: ${code}</strong></p>`,
          '<p>This code expires in 10 minutes. Do not share it with anyone.</p>',
        ].join('\n'),
      },
      {
        agent_id: null,
        owner_trusted: true,
      }
    );

    if (!emailResult || emailResult.ok === false) {
      console.warn('[artifact-acl] createOtpForShareLink: email_send returned non-ok:', emailResult);
      // OTP was stored — caller can retry email. Return partial success.
      return { ok: true, expires_in: 600, email_sent: false, email_error: emailResult?.status || 'unknown' };
    }
  } catch (emailErr) {
    console.warn('[artifact-acl] createOtpForShareLink: email_send failed:', emailErr.message);
    // OTP stored but email failed — surface gracefully.
    return { ok: true, expires_in: 600, email_sent: false, email_error: emailErr.message };
  }

  return { ok: true, expires_in: 600, email_sent: true };
}

// ── verifyOtpForShareLink ─────────────────────────────────────────────────────

/**
 * Verifies an OTP code for a given (linkId, email) pair.
 *
 * @param {string} linkId
 * @param {string} email
 * @param {string} code  The 6-digit string
 * @returns {{ ok: boolean, reason?: string }}
 */
export function verifyOtpForShareLink(linkId, email, code) {
  const db = getDb();

  // Find the most recent unexpired OTP for this (link, email).
  const otp = db
    .prepare(
      `SELECT * FROM artifact_share_link_otps
       WHERE share_link_id = ? AND email = ?
         AND expires_at > ?
         AND verified_at IS NULL
       ORDER BY created_at DESC
       LIMIT 1`
    )
    .get(linkId, email, nowIso());

  if (!otp) {
    return { ok: false, reason: 'not_found_or_expired' };
  }

  // Lockout after 5 failed attempts.
  if (otp.attempts >= 5) {
    try {
      appendComplianceAudit({
        user_id: null,
        org_id: 'personal-org', // best-effort without session context
        action: ComplianceActions.SHARE_OTP_LOCKED_OUT,
        action_target_id: linkId,
        action_target_type: 'artifact',
        status: 'denied',
        summary: `OTP lockout for ${email} on link ${linkId}`,
        meta: { share_link_id: linkId, attempts: otp.attempts },
      });
    } catch {}
    return { ok: false, reason: 'lockout' };
  }

  // Verify code against stored hash.
  const [salt, storedDigest] = otp.code_hash.split(':');
  const candidateDigest = createHash('sha256').update(salt + ':' + code).digest('hex');

  const saltBuf = Buffer.from(salt, 'utf8');
  const candidateHash = Buffer.from(candidateDigest, 'hex');
  const storedHash = Buffer.from(storedDigest, 'hex');

  // Constant-time compare on the hex digests.
  const match = candidateHash.length === storedHash.length && timingSafeEqual(candidateHash, storedHash);

  if (!match) {
    db.prepare('UPDATE artifact_share_link_otps SET attempts = attempts + 1 WHERE id = ?').run(otp.id);
    try {
      appendComplianceAudit({
        user_id: null,
        org_id: 'personal-org',
        action: ComplianceActions.SHARE_OTP_FAILED,
        action_target_id: linkId,
        action_target_type: 'artifact',
        status: 'denied',
        summary: `OTP verification failed for ${email} on link ${linkId}`,
        meta: { share_link_id: linkId, attempts: otp.attempts + 1 },
      });
    } catch {}
    return { ok: false, reason: 'invalid_code' };
  }

  // Mark as verified.
  db.prepare('UPDATE artifact_share_link_otps SET verified_at = ? WHERE id = ?').run(nowIso(), otp.id);

  try {
    appendComplianceAudit({
      user_id: null,
      org_id: 'personal-org',
      action: ComplianceActions.SHARE_OTP_VERIFIED,
      action_target_id: linkId,
      action_target_type: 'artifact',
      status: 'success',
      summary: `OTP verified for ${email} on link ${linkId}`,
      meta: { share_link_id: linkId },
    });
  } catch {}

  return { ok: true };
}
