/**
 * lib/space-acl.js — Public share links for a whole dashboard space.
 *
 * Mirrors lib/artifact-acl.js, but the grant is a space (dashboard_spaces.id)
 * rather than a single artifact. The crucial difference: the set of shared
 * artifacts is resolved LIVE at view time via listSpaceArtifactsForShare(), so
 * artifacts created in — or moved into — the space after the link was created
 * are included automatically. The space token IS the grant; the public viewer
 * intentionally bypasses per-artifact ACLs because the operator explicitly
 * shared the whole space and its future contents.
 *
 * Token design and secret are shared with artifact links via lib/share-secret.js,
 * so both link families use the same MADARX_SHARE_LINK_SECRET and IP rate limiter.
 *
 * Server-only: pulls node:sqlite via lib/db.js. Never import into client code.
 */

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, computeHmac, hmacMatch } from './share-secret.js';
import { publicShareUrl } from './share-url.js';

// ── Live artifact membership ──────────────────────────────────────────────────

/**
 * Returns the artifacts currently in a space. Resolved at call time so new and
 * moved-in artifacts are always reflected (the "future/moved" guarantee).
 *
 * @param {string} spaceId
 * @returns {object[]} [{ id, title, kind, content, created_at }]
 */
export function listSpaceArtifactsForShare(spaceId) {
  if (!spaceId) return [];
  return getDb()
    .prepare(
      `SELECT id, title, kind, content, source_md, created_at
         FROM artifacts
        WHERE space_id = ?
        ORDER BY created_at ASC, id ASC`
    )
    .all(String(spaceId))
    .map((a) => ({
      id: a.id,
      title: a.title || 'Untitled',
      kind: a.kind || 'text',
      content: a.content || a.source_md || '',
      created_at: a.created_at || null,
    }));
}

// ── createSpaceShareLink ──────────────────────────────────────────────────────

/**
 * Creates a signed public share link for a space.
 *
 * @param {string} spaceId  dashboard_spaces.id
 * @param {{ expires_in_days?: number, max_views?: number, require_email_otp?: boolean }} opts
 * @param {object} session  { user_id, org_id, ... }
 * @returns {Promise<{ id, token, url, expires_at, max_views, require_email_otp }>}
 */
export async function createSpaceShareLink(spaceId, opts = {}, session) {
  const { expires_in_days = 7, max_views = 50, require_email_otp } = opts;

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

  const db = getDb();

  const space = db.prepare('SELECT id FROM dashboard_spaces WHERE id = ?').get(String(spaceId));
  if (!space) throw { status: 404, error: 'space_not_found' };

  // require_email_otp: explicit opt wins, else 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;
  }

  const token = randomBytes(32).toString('base64url');
  const secret = await resolveShareSecret();
  const tokenHmac = computeHmac(token, secret);

  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('spaceshare');
  const orgId = session.org_id;

  db.prepare(
    `INSERT INTO space_share_links
       (id, space_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,
    String(spaceId),
    token,
    tokenHmac,
    session.user_id,
    orgId,
    expiresAt,
    max_views ?? null,
    emailOtp ? 1 : 0,
    nowIso()
  );

  try {
    appendComplianceAudit({
      user_id: session.user_id,
      org_id: orgId,
      action: ComplianceActions.SPACE_SHARE_LINK_CREATED,
      action_target_id: String(spaceId),
      action_target_type: 'space',
      status: 'success',
      summary: `Share link created for space ${spaceId}`,
      meta: { share_link_id: id, expires_at: expiresAt, max_views: max_views ?? null, require_email_otp: emailOtp },
    });
  } catch (auditErr) {
    console.error('[space-acl] audit (create) failed:', auditErr.message);
  }

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

// ── listSpaceShareLinks ───────────────────────────────────────────────────────

/**
 * Lists active links for a space. When orgId is provided, only links created by
 * that org are returned — dashboard spaces are global (no org_id column), so
 * org scoping is enforced on the link rows to prevent cross-org token exposure.
 */
export function listSpaceShareLinks(spaceId, orgId = null) {
  const db = getDb();
  if (orgId) {
    return db
      .prepare(
        `SELECT id, space_id, token, created_by, org_id, expires_at, max_views,
                access_count, revoked, require_email_otp, created_at
           FROM space_share_links
          WHERE space_id = ? AND org_id = ? AND revoked = 0
          ORDER BY created_at DESC`
      )
      .all(String(spaceId), orgId);
  }
  return db
    .prepare(
      `SELECT id, space_id, token, created_by, org_id, expires_at, max_views,
              access_count, revoked, require_email_otp, created_at
         FROM space_share_links
        WHERE space_id = ? AND revoked = 0
        ORDER BY created_at DESC`
    )
    .all(String(spaceId));
}

// ── verifySpaceShareLink ──────────────────────────────────────────────────────

/**
 * Verifies a space share token. Returns { ok, link, space } on success.
 * Artifacts are fetched separately (listSpaceArtifactsForShare) so they stay live.
 */
export async function verifySpaceShareLink(token) {
  if (!token) return { ok: false, status: 404, reason: 'not_found' };

  const secret = await resolveShareSecret();
  const candidateHmac = computeHmac(token, secret);

  const db = getDb();
  const link = db.prepare('SELECT * FROM space_share_links WHERE token_hmac = ?').get(candidateHmac);
  if (!link) return { ok: false, status: 404, reason: 'not_found' };
  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) {
    db.prepare('UPDATE space_share_links SET revoked = 1 WHERE id = ?').run(link.id);
    return { ok: false, status: 410, reason: 'view_cap_reached' };
  }

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

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

// ── recordSpaceShareLinkAccess ────────────────────────────────────────────────

export function recordSpaceShareLinkAccess(linkId, req, recipientEmail = null) {
  const db = getDb();
  const result = db.prepare(
    `UPDATE space_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;

  const link = db.prepare('SELECT * FROM space_share_links WHERE id = ?').get(linkId);
  if (!link) return;

  try {
    appendComplianceAudit({
      user_id: null,
      org_id: link.org_id,
      action: ComplianceActions.SPACE_SHARE_LINK_ACCESSED,
      action_target_id: link.space_id,
      action_target_type: 'space',
      status: 'success',
      summary: `Space share link ${linkId} accessed${recipientEmail ? ` by ${recipientEmail}` : ''}`,
      meta: { share_link_id: linkId, access_count: link.access_count, auto_revoked: link.revoked === 1, recipient_email: recipientEmail },
      ip: requestIp(req),
      user_agent: requestUserAgent(req),
    });
  } catch (auditErr) {
    console.error('[space-acl] audit (access) failed:', auditErr.message);
  }
}

// ── revokeSpaceShareLink ──────────────────────────────────────────────────────

export function revokeSpaceShareLink(linkId, session) {
  const db = getDb();
  const link = db.prepare('SELECT * FROM space_share_links WHERE id = ?').get(linkId);
  if (!link) throw { status: 404, error: 'share_link_not_found' };

  db.prepare('UPDATE space_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.SPACE_SHARE_LINK_REVOKED,
      action_target_id: link.space_id,
      action_target_type: 'space',
      status: 'success',
      summary: `Space share link ${linkId} revoked`,
      meta: { share_link_id: linkId, space_id: link.space_id },
    });
  } catch (auditErr) {
    console.error('[space-acl] audit (revoke) failed:', auditErr.message);
  }

  return { ok: true };
}

/** Revokes all active links for a space (called on space delete). */
export function revokeAllSpaceShareLinks(spaceId) {
  return getDb()
    .prepare('UPDATE space_share_links SET revoked = 1 WHERE space_id = ? AND revoked = 0')
    .run(String(spaceId)).changes;
}

// ── OTP (mirrors artifact-acl) ────────────────────────────────────────────────

export async function createOtpForSpaceShareLink(linkId, email) {
  const db = getDb();
  const link = db.prepare('SELECT * FROM space_share_links WHERE id = ?').get(linkId);
  if (!link || link.revoked === 1) return { ok: false, error: 'link_invalid' };

  const code = String(Math.floor(100000 + (randomBytes(3).readUIntBE(0, 3) % 900000))).padStart(6, '0');
  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();

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

  try {
    appendComplianceAudit({
      user_id: null,
      org_id: link.org_id,
      action: ComplianceActions.SHARE_OTP_SENT,
      action_target_id: link.space_id,
      action_target_type: 'space',
      status: 'success',
      summary: `Space share OTP sent to ${email} for link ${linkId}`,
      meta: { share_link_id: linkId, otp_id: id },
    });
  } catch { /* non-fatal */ }

  try {
    const { executeRegistryTool } = await import('./tools/registry.js');
    const emailResult = await executeRegistryTool(
      'email_send',
      {
        to: email,
        subject: 'Your madarX shared space verification code',
        html: [
          '<p>You requested access to a shared space 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) {
      return { ok: true, expires_in: 600, email_sent: false, email_error: emailResult?.status || 'unknown' };
    }
  } catch (emailErr) {
    return { ok: true, expires_in: 600, email_sent: false, email_error: emailErr.message };
  }

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

export function verifyOtpForSpaceShareLink(linkId, email, code) {
  const db = getDb();
  const otp = db
    .prepare(
      `SELECT * FROM space_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' };

  if (otp.attempts >= 5) {
    try {
      appendComplianceAudit({
        user_id: null, org_id: 'personal-org',
        action: ComplianceActions.SHARE_OTP_LOCKED_OUT,
        action_target_id: linkId, action_target_type: 'space', status: 'denied',
        summary: `Space share OTP lockout for ${email} on link ${linkId}`,
        meta: { share_link_id: linkId, attempts: otp.attempts },
      });
    } catch { /* non-fatal */ }
    return { ok: false, reason: 'lockout' };
  }

  const [salt, storedDigest] = otp.code_hash.split(':');
  const candidateDigest = createHash('sha256').update(salt + ':' + code).digest('hex');
  const candidateHash = Buffer.from(candidateDigest, 'hex');
  const storedHash = Buffer.from(storedDigest, 'hex');
  const match = candidateHash.length === storedHash.length && timingSafeEqual(candidateHash, storedHash);

  if (!match) {
    db.prepare('UPDATE space_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: 'space', status: 'denied',
        summary: `Space share OTP failed for ${email} on link ${linkId}`,
        meta: { share_link_id: linkId, attempts: otp.attempts + 1 },
      });
    } catch { /* non-fatal */ }
    return { ok: false, reason: 'invalid_code' };
  }

  db.prepare('UPDATE space_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: 'space', status: 'success',
      summary: `Space share OTP verified for ${email} on link ${linkId}`,
      meta: { share_link_id: linkId },
    });
  } catch { /* non-fatal */ }

  return { ok: true };
}
