import Database from 'better-sqlite3';
import { join } from 'path';

// ── Types ────────────────────────────────────────────────────────

export type Role = 'pilot' | 'cab' | 'fenja';
export type ContributionType = 'idea' | 'inspiration' | 'question';
export type AttendanceStatus = 'yes' | 'no';

export interface User {
  id: number;
  email: string;
  password_hash: string;
  name: string;
  organisation: string;
  role: Role;
  bio: string;
  created_at: string;
  last_seen_at: string | null;
  active: number;
  title: string | null;            // job title, e.g. 'VP Engineering'
  cab_joined_date: string | null;  // ISO date; only set for role='cab'. Falls back to created_at when null.
  slug: string | null;             // URL handle, kebab-case, unique. Auto-generated on createUser.
  pull_quote: string | null;       // one sentence in the member's voice, rendered on /members
  member_number: number | null;    // sequential, never reused. Allocated on role→cab transition.
  focus_tags: string | null;       // JSON array of up to 3 strings (24 chars max each)
}

export type UserPublic = Omit<User, 'password_hash'>;

export interface Invite {
  id: number;
  token_hash: string;
  email: string;
  name: string;
  organisation: string;
  role: Role;
  expires_at: string;
  used_at: string | null;
  created_at: string;
  created_by_user_id: number | null;
}

export interface Contribution {
  id: number;
  user_id: number;
  type: ContributionType;
  body_md: string;
  created_at: string;
  edited_at: string | null;
  hidden_at: string | null;
}

export interface ContributionRow extends Contribution {
  author_name: string;
  author_organisation: string;
  author_role: Role;
  reaction_count: number;
}

export interface Reply {
  id: number;
  contribution_id: number;
  user_id: number;
  body_md: string;
  created_at: string;
  author_name: string;
  author_role: Role;
}

export interface AttendanceSummary {
  yes: number;
  no: number;
}

// ── Connection ───────────────────────────────────────────────────

// Persist across Vite HMR reloads in dev. Override BIFROST_DB_PATH for tests
// (use ':memory:'); defaults to bifrost.db in the project root.
const DB_PATH = process.env.BIFROST_DB_PATH ?? join(process.cwd(), 'bifrost.db');

const g = globalThis as typeof globalThis & { __bifrost_db?: Database.Database };
if (!g.__bifrost_db) {
  g.__bifrost_db = new Database(DB_PATH);
  g.__bifrost_db.pragma('journal_mode = WAL');
  g.__bifrost_db.pragma('foreign_keys = ON');
}
const db = g.__bifrost_db;

// ── Users ────────────────────────────────────────────────────────

export function getUserByEmail(email: string): User | null {
  return db.prepare(
    'SELECT * FROM users WHERE email = ? AND active = 1'
  ).get(email) as User | null;
}

export function getUserById(id: number): User | null {
  return db.prepare(
    'SELECT * FROM users WHERE id = ? AND active = 1'
  ).get(id) as User | null;
}

export function getUserPublicById(id: number): UserPublic | null {
  return db.prepare(
    'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug,pull_quote,member_number,focus_tags FROM users WHERE id = ? AND active = 1'
  ).get(id) as UserPublic | null;
}

export function createUser(data: {
  email: string;
  password_hash: string;
  name: string;
  organisation: string;
  role: Role;
}): number {
  const slug = generateUniqueSlug(data.name);
  const cabJoined = data.role === 'cab' ? new Date().toISOString().slice(0, 10) : null;
  const tx = db.transaction(() => {
    const r = db.prepare(
      'INSERT INTO users (email,password_hash,name,organisation,role,slug,cab_joined_date) VALUES (?,?,?,?,?,?,?)'
    ).run(data.email, data.password_hash, data.name, data.organisation, data.role, slug, cabJoined);
    const id = Number(r.lastInsertRowid);
    if (data.role === 'cab') allocateMemberNumber(id);
    return id;
  });
  return tx();
}

/**
 * Reserve the next free member_number for a user. Strictly sequential,
 * never reused — even if a number-holder is deactivated and later replaced,
 * the new member gets MAX(member_number) + 1, not the retired number.
 * Idempotent: returns the existing number if already allocated.
 */
export function allocateMemberNumber(userId: number): number {
  return db.transaction(() => {
    const existing = db.prepare('SELECT member_number FROM users WHERE id = ?').get(userId) as { member_number: number | null } | undefined;
    if (existing?.member_number != null) return existing.member_number;
    const max = db.prepare('SELECT MAX(member_number) AS m FROM users').get() as { m: number | null };
    const next = (max.m ?? 0) + 1;
    db.prepare('UPDATE users SET member_number = ? WHERE id = ?').run(next, userId);
    return next;
  })();
}

export function updateUserLastSeen(id: number): void {
  db.prepare("UPDATE users SET last_seen_at = datetime('now') WHERE id = ?").run(id);
}

export function updateUserProfile(id: number, name: string, bio: string): void {
  db.prepare('UPDATE users SET name = ?, bio = ? WHERE id = ?').run(name, bio, id);
}

/** Returns the newly-allocated member_number when the transition lands on
 *  cab and the user had none; null otherwise. Callers may ignore. */
export function updateUserRole(id: number, role: Role): { allocated: number | null } {
  return db.transaction(() => {
    const current = db.prepare('SELECT role, member_number FROM users WHERE id = ?')
      .get(id) as { role: Role; member_number: number | null } | undefined;
    if (!current) return { allocated: null };
    db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id);
    if (role === 'cab' && current.member_number === null) {
      return { allocated: allocateMemberNumber(id) };
    }
    return { allocated: null };
  })();
}

export function deactivateUser(id: number): void {
  db.prepare('UPDATE users SET active = 0 WHERE id = ?').run(id);
}

export function getAllUsersPublic(): UserPublic[] {
  return db.prepare(
    'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug,pull_quote,member_number,focus_tags FROM users ORDER BY organisation,name'
  ).all() as UserPublic[];
}

export function getUserBySlug(slug: string): UserPublic | null {
  return db.prepare(
    'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug,pull_quote,member_number,focus_tags FROM users WHERE slug = ? AND active = 1'
  ).get(slug) as UserPublic | null;
}

/** All cab users, ordered by member_number asc (with NULLs at the end as a defensive fallback). */
export function getAllCabMembers(): UserPublic[] {
  return db.prepare(`
    SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug,pull_quote,member_number,focus_tags
    FROM users
    WHERE role = 'cab' AND active = 1
    ORDER BY (member_number IS NULL), member_number ASC, id ASC
  `).all() as UserPublic[];
}

export function countCabMembers(): number {
  const r = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'cab' AND active = 1").get() as { n: number };
  return r.n;
}

/** Update admin-editable profile fields (title, pull_quote, focus_tags). focus_tags is a parsed array. */
export function updateUserAdminFields(id: number, data: {
  title: string | null;
  pull_quote: string | null;
  focus_tags: string[];
}): void {
  db.prepare('UPDATE users SET title = ?, pull_quote = ?, focus_tags = ? WHERE id = ?')
    .run(data.title, data.pull_quote, JSON.stringify(data.focus_tags), id);
}

// ── Slugs ────────────────────────────────────────────────────────

export function slugifyName(name: string): string {
  return name
    .toLowerCase()
    .normalize('NFKD').replace(/[̀-ͯ]/g, '') // strip diacritics
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

/** Returns a slug guaranteed not to collide with any existing user. */
export function generateUniqueSlug(name: string): string {
  const base = slugifyName(name) || 'member';
  let candidate = base;
  let n = 1;
  while (db.prepare('SELECT 1 FROM users WHERE slug = ?').get(candidate)) {
    n += 1;
    candidate = `${base}-${n}`;
  }
  return candidate;
}

// ── Sessions ─────────────────────────────────────────────────────

export function createDbSession(id: string, userId: number, expiresAt: string): void {
  db.prepare(
    'INSERT INTO sessions (id,user_id,expires_at) VALUES (?,?,?)'
  ).run(id, userId, expiresAt);
}

export function getDbSession(id: string): { user_id: number; expires_at: string } | null {
  return db.prepare(
    "SELECT user_id,expires_at FROM sessions WHERE id = ? AND expires_at > datetime('now')"
  ).get(id) as { user_id: number; expires_at: string } | null;
}

export function deleteDbSession(id: string): void {
  db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
}

// ── Invites ──────────────────────────────────────────────────────

export function createInvite(data: {
  token_hash: string;
  email: string;
  name: string;
  organisation: string;
  role: Role;
  expires_at: string;
  created_by_user_id: number;
}): number {
  const r = db.prepare(`
    INSERT INTO invites (token_hash,email,name,organisation,role,expires_at,created_by_user_id)
    VALUES (?,?,?,?,?,?,?)
  `).run(data.token_hash, data.email, data.name, data.organisation, data.role, data.expires_at, data.created_by_user_id);
  return Number(r.lastInsertRowid);
}

export function getInviteByTokenHash(tokenHash: string): Invite | null {
  return db.prepare(
    'SELECT * FROM invites WHERE token_hash = ?'
  ).get(tokenHash) as Invite | null;
}

export function redeemInvite(inviteId: number): void {
  db.prepare("UPDATE invites SET used_at = datetime('now') WHERE id = ?").run(inviteId);
}

export function createUserFromInvite(invite: Invite, passwordHash: string): UserPublic {
  const id = createUser({
    email: invite.email,
    password_hash: passwordHash,
    name: invite.name,
    organisation: invite.organisation,
    role: invite.role,
  });
  return getUserPublicById(id) as UserPublic;
}

export function markInviteUsed(id: number): void {
  db.prepare("UPDATE invites SET used_at = datetime('now') WHERE id = ?").run(id);
}

export function revokeInvite(id: number): void {
  db.prepare("UPDATE invites SET expires_at = datetime('now') WHERE id = ? AND used_at IS NULL").run(id);
}

export function getAllInvites(): (Invite & { creator_name: string | null })[] {
  return db.prepare(`
    SELECT i.*, u.name AS creator_name
    FROM invites i
    LEFT JOIN users u ON u.id = i.created_by_user_id
    ORDER BY i.created_at DESC
  `).all() as (Invite & { creator_name: string | null })[];
}

// ── Contributions ────────────────────────────────────────────────

export function createContribution(data: {
  user_id: number;
  type: ContributionType;
  body_md: string;
}): number {
  const r = db.prepare(
    'INSERT INTO contributions (user_id,type,body_md) VALUES (?,?,?)'
  ).run(data.user_id, data.type, data.body_md);
  return Number(r.lastInsertRowid);
}

export function getContributions(opts: {
  type?: ContributionType;
  sort?: 'newest' | 'top';
} = {}): ContributionRow[] {
  const typeClause = opts.type ? `AND c.type = '${opts.type}'` : '';
  const orderClause = opts.sort === 'top'
    ? 'ORDER BY reaction_count DESC, c.created_at DESC'
    : 'ORDER BY c.created_at DESC';
  return db.prepare(`
    SELECT
      c.*,
      u.name  AS author_name,
      u.organisation AS author_organisation,
      u.role  AS author_role,
      (SELECT COUNT(*) FROM reactions r WHERE r.contribution_id = c.id) AS reaction_count
    FROM contributions c
    JOIN users u ON u.id = c.user_id
    WHERE c.hidden_at IS NULL ${typeClause}
    ${orderClause}
  `).all() as ContributionRow[];
}

export function getContributionById(id: number): Contribution | null {
  return db.prepare('SELECT * FROM contributions WHERE id = ?').get(id) as Contribution | null;
}

export function updateContribution(id: number, body_md: string): void {
  db.prepare("UPDATE contributions SET body_md = ?, edited_at = datetime('now') WHERE id = ?")
    .run(body_md, id);
}

export function hideContribution(id: number): void {
  db.prepare("UPDATE contributions SET hidden_at = datetime('now') WHERE id = ?").run(id);
}

// ── Reactions ────────────────────────────────────────────────────

export function addReaction(userId: number, contributionId: number): void {
  db.prepare('INSERT OR IGNORE INTO reactions (user_id,contribution_id) VALUES (?,?)').run(userId, contributionId);
}

export function removeReaction(userId: number, contributionId: number): void {
  db.prepare('DELETE FROM reactions WHERE user_id = ? AND contribution_id = ?').run(userId, contributionId);
}

export function hasReaction(userId: number, contributionId: number): boolean {
  const r = db.prepare('SELECT id FROM reactions WHERE user_id = ? AND contribution_id = ?').get(userId, contributionId);
  return r !== undefined;
}

export function getReactedIds(userId: number): Set<number> {
  const rows = db.prepare('SELECT contribution_id FROM reactions WHERE user_id = ?').all(userId) as { contribution_id: number }[];
  return new Set(rows.map(r => r.contribution_id));
}

// ── Replies ──────────────────────────────────────────────────────

export function createReply(data: {
  contribution_id: number;
  user_id: number;
  body_md: string;
}): void {
  db.prepare('INSERT INTO replies (contribution_id,user_id,body_md) VALUES (?,?,?)')
    .run(data.contribution_id, data.user_id, data.body_md);
}

export function getReplies(contributionId: number): Reply[] {
  return db.prepare(`
    SELECT re.*,u.name AS author_name,u.role AS author_role
    FROM replies re
    JOIN users u ON u.id = re.user_id
    WHERE re.contribution_id = ?
    ORDER BY re.created_at ASC
  `).all(contributionId) as Reply[];
}

// ── Attendance ───────────────────────────────────────────────────

export function setAttendance(userId: number, meetingSlug: string, status: AttendanceStatus): void {
  db.prepare(`
    INSERT INTO attendance (user_id,meeting_slug,status,updated_at)
    VALUES (?,?,?,datetime('now'))
    ON CONFLICT(user_id,meeting_slug) DO UPDATE SET status=excluded.status, updated_at=excluded.updated_at
  `).run(userId, meetingSlug, status);
}

export function getAttendanceSummary(meetingSlug: string): AttendanceSummary {
  const rows = db.prepare(
    "SELECT status, COUNT(*) AS n FROM attendance WHERE meeting_slug = ? GROUP BY status"
  ).all(meetingSlug) as { status: string; n: number }[];
  const yes = rows.find(r => r.status === 'yes')?.n ?? 0;
  const no  = rows.find(r => r.status === 'no')?.n ?? 0;
  return { yes, no };
}

export function getUserAttendance(userId: number, meetingSlug: string): AttendanceStatus | null {
  const r = db.prepare(
    'SELECT status FROM attendance WHERE user_id = ? AND meeting_slug = ?'
  ).get(userId, meetingSlug) as { status: AttendanceStatus } | undefined;
  return r?.status ?? null;
}

export function getAllAttendance(meetingSlug: string): { user_id: number; status: AttendanceStatus; name: string }[] {
  return db.prepare(`
    SELECT a.user_id, a.status, u.name
    FROM attendance a JOIN users u ON u.id = a.user_id
    WHERE a.meeting_slug = ?
  `).all(meetingSlug) as { user_id: number; status: AttendanceStatus; name: string }[];
}

// ── Join requests ────────────────────────────────────────────────

export function createJoinRequest(userId: number): void {
  db.prepare('INSERT OR IGNORE INTO join_requests (user_id) VALUES (?)').run(userId);
}

export function hasJoinRequest(userId: number): boolean {
  const r = db.prepare('SELECT id FROM join_requests WHERE user_id = ?').get(userId);
  return r !== undefined;
}

export interface JoinRequest {
  id: number;
  user_id: number;
  created_at: string;
  user_name: string;
  user_email: string;
  user_organisation: string;
}

export function getAllJoinRequests(): JoinRequest[] {
  return db.prepare(`
    SELECT jr.id, jr.user_id, jr.created_at,
           u.name AS user_name, u.email AS user_email, u.organisation AS user_organisation
    FROM join_requests jr
    JOIN users u ON u.id = jr.user_id
    ORDER BY jr.created_at DESC
  `).all() as JoinRequest[];
}

// ── Date helpers ─────────────────────────────────────────────────

/** SQLite stores 'YYYY-MM-DD HH:MM:SS' in UTC by project convention. */
function parseSqlDate(s: string): Date {
  if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
  return new Date(s.replace(' ', 'T') + 'Z');
}

// ── Pulses ───────────────────────────────────────────────────────

export type PulseStatus = 'draft' | 'open' | 'closed';

export interface Pulse {
  id: number;
  question: string;
  context: string | null;
  options: string;       // raw JSON
  opens_at: string;
  closes_at: string;
  status: PulseStatus;   // stored status
  created_at: string;
  created_by: number;
}

export interface PulseRow {
  id: number;
  question: string;
  context: string | null;
  options: string[];     // parsed
  opens_at: string;
  closes_at: string;
  stored_status: PulseStatus;
  status: PulseStatus;   // derived from stored + dates
  created_at: string;
  created_by: number;
}

export interface PulseWithCounts extends PulseRow {
  votes_total: number;
  votes_by_option: number[];
  my_vote: number | null;
}

/**
 * derive the live status of a pulse from its stored status and dates.
 *  - draft  → stays draft until admin publishes (independent of dates)
 *  - closed → stays closed
 *  - open   → becomes closed once now ≥ closes_at
 * Promoting draft → open is an explicit admin action (publishPulse), not date-driven.
 */
export function derivePulseStatus(
  stored: PulseStatus,
  closesAt: string,
  now: Date = new Date()
): PulseStatus {
  if (stored === 'draft' || stored === 'closed') return stored;
  return now < parseSqlDate(closesAt) ? 'open' : 'closed';
}

function rowToPulse(row: Pulse): PulseRow {
  const status = derivePulseStatus(row.status, row.closes_at);
  return {
    id: row.id,
    question: row.question,
    context: row.context,
    options: JSON.parse(row.options) as string[],
    opens_at: row.opens_at,
    closes_at: row.closes_at,
    stored_status: row.status,
    status,
    created_at: row.created_at,
    created_by: row.created_by,
  };
}

export function createPulse(data: {
  question: string;
  context: string | null;
  options: string[];
  opens_at: string;
  closes_at: string;
  status?: PulseStatus;
  created_by: number;
}): number {
  if (data.options.length < 2 || data.options.length > 4) {
    throw new Error('Pulse must have between 2 and 4 options');
  }
  const r = db.prepare(`
    INSERT INTO pulses (question, context, options, opens_at, closes_at, status, created_by)
    VALUES (?,?,?,?,?,?,?)
  `).run(
    data.question,
    data.context,
    JSON.stringify(data.options),
    data.opens_at,
    data.closes_at,
    data.status ?? 'draft',
    data.created_by,
  );
  return Number(r.lastInsertRowid);
}

export function updatePulse(id: number, data: {
  question: string;
  context: string | null;
  options: string[];
  opens_at: string;
  closes_at: string;
}): void {
  db.prepare(`
    UPDATE pulses SET question = ?, context = ?, options = ?, opens_at = ?, closes_at = ?
    WHERE id = ?
  `).run(data.question, data.context, JSON.stringify(data.options), data.opens_at, data.closes_at, id);
}

export function publishPulse(id: number): void {
  db.prepare("UPDATE pulses SET status = 'open' WHERE id = ? AND status = 'draft'").run(id);
}

export function closePulse(id: number): void {
  db.prepare("UPDATE pulses SET status = 'closed' WHERE id = ?").run(id);
}

export function deletePulse(id: number): void {
  db.prepare('DELETE FROM pulses WHERE id = ?').run(id);
}

export function getPulseById(id: number): PulseRow | null {
  const row = db.prepare('SELECT * FROM pulses WHERE id = ?').get(id) as Pulse | undefined;
  return row ? rowToPulse(row) : null;
}

export function getAllPulses(): PulseRow[] {
  const rows = db.prepare('SELECT * FROM pulses ORDER BY created_at DESC').all() as Pulse[];
  return rows.map(rowToPulse);
}

/** The current open pulse, if any. Returns null if none is open right now. */
export function getOpenPulse(): PulseRow | null {
  const rows = db.prepare(
    "SELECT * FROM pulses WHERE status = 'open' ORDER BY closes_at ASC LIMIT 1"
  ).all() as Pulse[];
  for (const row of rows) {
    const live = rowToPulse(row);
    if (live.status === 'open') return live;
  }
  return null;
}

export function getPulseWithCounts(id: number, userId: number): PulseWithCounts | null {
  const pulse = getPulseById(id);
  if (!pulse) return null;
  const counts = db.prepare(
    'SELECT option_index, COUNT(*) AS n FROM votes WHERE pulse_id = ? GROUP BY option_index'
  ).all(id) as { option_index: number; n: number }[];
  const byOption = pulse.options.map((_, i) => counts.find(c => c.option_index === i)?.n ?? 0);
  const myVoteRow = db.prepare(
    'SELECT option_index FROM votes WHERE pulse_id = ? AND user_id = ?'
  ).get(id, userId) as { option_index: number } | undefined;
  return {
    ...pulse,
    votes_total: byOption.reduce((a, b) => a + b, 0),
    votes_by_option: byOption,
    my_vote: myVoteRow?.option_index ?? null,
  };
}

// ── Votes ────────────────────────────────────────────────────────

export function castVote(pulseId: number, userId: number, optionIndex: number): void {
  // UNIQUE(pulse_id, user_id) enforces one-per-member. INSERT OR IGNORE keeps
  // the first vote and silently no-ops on re-attempts; callers should read back.
  db.prepare(
    'INSERT OR IGNORE INTO votes (pulse_id, user_id, option_index) VALUES (?,?,?)'
  ).run(pulseId, userId, optionIndex);
}

export function getUserVote(pulseId: number, userId: number): number | null {
  const r = db.prepare(
    'SELECT option_index FROM votes WHERE pulse_id = ? AND user_id = ?'
  ).get(pulseId, userId) as { option_index: number } | undefined;
  return r?.option_index ?? null;
}

export function countPulseParticipants(pulseId: number): number {
  const r = db.prepare(
    'SELECT COUNT(*) AS n FROM votes WHERE pulse_id = ?'
  ).get(pulseId) as { n: number };
  return r.n;
}

// ── Roadmap items ────────────────────────────────────────────────

export type RoadmapStatus = 'shipping' | 'beta' | 'exploring';

export interface RoadmapItem {
  id: number;
  title: string;
  description: string;
  status: RoadmapStatus;
  target: string | null;
  display_order: number;
  shipped_at: string | null;
  created_at: string;
  updated_at: string;
}

export interface RoadmapItemWithAttribution extends RoadmapItem {
  attributed: { id: number; name: string; slug: string | null }[];
}

export function createRoadmapItem(data: {
  title: string;
  description: string;
  status: RoadmapStatus;
  target?: string | null;
  display_order?: number;
}): number {
  const shipped_at = data.status === 'shipping' ? new Date().toISOString().slice(0, 19).replace('T', ' ') : null;
  const r = db.prepare(`
    INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at)
    VALUES (?,?,?,?,?,?)
  `).run(
    data.title,
    data.description,
    data.status,
    data.target ?? null,
    data.display_order ?? 0,
    shipped_at,
  );
  return Number(r.lastInsertRowid);
}

/**
 * Update a roadmap item. If status transitions to 'shipping' for the first
 * time (shipped_at IS NULL), stamps shipped_at = now. Never resets shipped_at.
 */
export function updateRoadmapItem(id: number, data: {
  title: string;
  description: string;
  status: RoadmapStatus;
  target: string | null;
  display_order: number;
}): { shippedNow: boolean } {
  const current = db.prepare('SELECT status, shipped_at FROM roadmap_items WHERE id = ?')
    .get(id) as { status: RoadmapStatus; shipped_at: string | null } | undefined;
  if (!current) throw new Error(`Roadmap item ${id} not found`);

  const shippedNow = data.status === 'shipping' && current.shipped_at === null;
  const shipped_at = shippedNow
    ? new Date().toISOString().slice(0, 19).replace('T', ' ')
    : current.shipped_at;

  db.prepare(`
    UPDATE roadmap_items
       SET title = ?, description = ?, status = ?, target = ?, display_order = ?,
           shipped_at = ?, updated_at = datetime('now')
     WHERE id = ?
  `).run(data.title, data.description, data.status, data.target, data.display_order, shipped_at, id);

  return { shippedNow };
}

export function deleteRoadmapItem(id: number): void {
  db.prepare('DELETE FROM roadmap_items WHERE id = ?').run(id);
}

export function getRoadmapItem(id: number): RoadmapItemWithAttribution | null {
  const item = db.prepare('SELECT * FROM roadmap_items WHERE id = ?').get(id) as RoadmapItem | undefined;
  if (!item) return null;
  const attributed = db.prepare(`
    SELECT u.id, u.name, u.slug FROM roadmap_attributions ra
    JOIN users u ON u.id = ra.user_id
    WHERE ra.roadmap_item_id = ?
    ORDER BY u.name
  `).all(id) as { id: number; name: string; slug: string | null }[];
  return { ...item, attributed };
}

export function getAllRoadmapItems(): RoadmapItemWithAttribution[] {
  const items = db.prepare(
    'SELECT * FROM roadmap_items ORDER BY status, display_order, created_at'
  ).all() as RoadmapItem[];
  const attribs = db.prepare(`
    SELECT ra.roadmap_item_id, u.id, u.name, u.slug
    FROM roadmap_attributions ra
    JOIN users u ON u.id = ra.user_id
    ORDER BY u.name
  `).all() as { roadmap_item_id: number; id: number; name: string; slug: string | null }[];
  const byItem = new Map<number, { id: number; name: string; slug: string | null }[]>();
  for (const a of attribs) {
    const list = byItem.get(a.roadmap_item_id) ?? [];
    list.push({ id: a.id, name: a.name, slug: a.slug });
    byItem.set(a.roadmap_item_id, list);
  }
  return items.map(i => ({ ...i, attributed: byItem.get(i.id) ?? [] }));
}

export function setRoadmapAttributions(itemId: number, userIds: number[]): void {
  const tx = db.transaction(() => {
    db.prepare('DELETE FROM roadmap_attributions WHERE roadmap_item_id = ?').run(itemId);
    const ins = db.prepare('INSERT INTO roadmap_attributions (roadmap_item_id, user_id) VALUES (?,?)');
    for (const uid of userIds) ins.run(itemId, uid);
  });
  tx();
}

// ── Events ───────────────────────────────────────────────────────

export type EventKind = 'dinner' | 'office_hours' | 'summit' | 'virtual' | 'working_session';

export interface Event {
  id: number;
  slug: string;
  title: string;
  kind: EventKind;
  description: string;
  location: string;
  starts_at: string;
  ends_at: string | null;
  capacity: number | null;
  photo_url: string | null;
  audience: string | null;
  duration_label: string | null;
  action_label: string | null;
  notes_url: string | null;
  created_at: string;
  created_by: number | null;
}

export function createEvent(data: {
  slug: string;
  title: string;
  kind: EventKind;
  description: string;
  location: string;
  starts_at: string;
  ends_at: string | null;
  capacity: number | null;
  photo_url: string | null;
  audience?: string | null;
  duration_label?: string | null;
  action_label?: string | null;
  notes_url?: string | null;
  created_by: number;
}): number {
  const r = db.prepare(`
    INSERT INTO events (slug, title, kind, description, location, starts_at, ends_at, capacity, photo_url, audience, duration_label, action_label, notes_url, created_by)
    VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
  `).run(
    data.slug, data.title, data.kind, data.description, data.location,
    data.starts_at, data.ends_at, data.capacity, data.photo_url,
    data.audience ?? null, data.duration_label ?? null, data.action_label ?? null, data.notes_url ?? null,
    data.created_by,
  );
  return Number(r.lastInsertRowid);
}

export function updateEvent(id: number, data: {
  title: string;
  kind: EventKind;
  description: string;
  location: string;
  starts_at: string;
  ends_at: string | null;
  capacity: number | null;
  photo_url: string | null;
  audience?: string | null;
  duration_label?: string | null;
  action_label?: string | null;
  notes_url?: string | null;
}): void {
  db.prepare(`
    UPDATE events SET title = ?, kind = ?, description = ?, location = ?,
                      starts_at = ?, ends_at = ?, capacity = ?, photo_url = ?,
                      audience = ?, duration_label = ?, action_label = ?, notes_url = ?
    WHERE id = ?
  `).run(
    data.title, data.kind, data.description, data.location,
    data.starts_at, data.ends_at, data.capacity, data.photo_url,
    data.audience ?? null, data.duration_label ?? null, data.action_label ?? null, data.notes_url ?? null,
    id,
  );
}

export function deleteEvent(id: number): void {
  db.prepare('DELETE FROM events WHERE id = ?').run(id);
}

export function getEventById(id: number): Event | null {
  return db.prepare('SELECT * FROM events WHERE id = ?').get(id) as Event | null;
}

export function getEventBySlug(slug: string): Event | null {
  return db.prepare('SELECT * FROM events WHERE slug = ?').get(slug) as Event | null;
}

export function getAllEvents(): Event[] {
  return db.prepare('SELECT * FROM events ORDER BY starts_at DESC').all() as Event[];
}

export function getUpcomingEvents(limit = 10): Event[] {
  return db.prepare(
    "SELECT * FROM events WHERE starts_at >= datetime('now') ORDER BY starts_at ASC LIMIT ?"
  ).all(limit) as Event[];
}

export function getPastEvents(limit = 50): Event[] {
  return db.prepare(
    "SELECT * FROM events WHERE starts_at < datetime('now') ORDER BY starts_at DESC LIMIT ?"
  ).all(limit) as Event[];
}

export function getEventRsvpCount(slug: string): { going: number; interested: number; declined: number } {
  const rows = db.prepare(
    "SELECT status, COUNT(*) AS n FROM attendance WHERE meeting_slug = ? AND kind = 'event' GROUP BY status"
  ).all(slug) as { status: string; n: number }[];
  return {
    going: rows.find(r => r.status === 'yes')?.n ?? 0,
    interested: rows.find(r => r.status === 'interested')?.n ?? 0,
    declined: rows.find(r => r.status === 'no')?.n ?? 0,
  };
}

export function setEventRsvp(userId: number, eventSlug: string, status: 'yes' | 'no' | 'interested'): void {
  db.prepare(`
    INSERT INTO attendance (user_id, meeting_slug, kind, status, updated_at)
    VALUES (?, ?, 'event', ?, datetime('now'))
    ON CONFLICT(user_id, meeting_slug) DO UPDATE SET status = excluded.status, updated_at = excluded.updated_at
  `).run(userId, eventSlug, status);
}

export function getUserRsvp(userId: number, eventSlug: string): 'yes' | 'no' | 'interested' | null {
  const r = db.prepare(
    "SELECT status FROM attendance WHERE user_id = ? AND meeting_slug = ? AND kind = 'event'"
  ).get(userId, eventSlug) as { status: 'yes' | 'no' | 'interested' } | undefined;
  return r?.status ?? null;
}

// ── Activity (ticker feed) ───────────────────────────────────────

export type ActivityKind = 'voted' | 'rsvped' | 'booked_office_hours' | 'roadmap_shipped' | 'pulse_opened';

export interface Activity {
  id: number;
  actor_id: number;
  kind: ActivityKind;
  subject_type: string;
  subject_id: number;
  created_at: string;
}

export interface ActivityRow extends Activity {
  actor_name: string;
  actor_role: Role;
}

export function recordActivity(
  actorId: number,
  kind: ActivityKind,
  subjectType: string,
  subjectId: number,
): void {
  db.prepare(
    'INSERT INTO activity (actor_id, kind, subject_type, subject_id) VALUES (?,?,?,?)'
  ).run(actorId, kind, subjectType, subjectId);
}

export function getRecentActivity(opts: { limit?: number; sinceDays?: number } = {}): ActivityRow[] {
  const limit = opts.limit ?? 12;
  const sinceDays = opts.sinceDays ?? 7;
  return db.prepare(`
    SELECT a.*, u.name AS actor_name, u.role AS actor_role
    FROM activity a JOIN users u ON u.id = a.actor_id
    WHERE a.created_at >= datetime('now', ?)
    ORDER BY a.created_at DESC
    LIMIT ?
  `).all(`-${sinceDays} days`, limit) as ActivityRow[];
}

export function getAllActivityForAdmin(limit = 200): ActivityRow[] {
  return db.prepare(`
    SELECT a.*, u.name AS actor_name, u.role AS actor_role
    FROM activity a JOIN users u ON u.id = a.actor_id
    ORDER BY a.created_at DESC
    LIMIT ?
  `).all(limit) as ActivityRow[];
}

// ── Council mark data ────────────────────────────────────────────

/**
 * Returns a set of quarter indices (0 = current quarter, 1 = previous, …, up to 11)
 * in which the user has at least one roadmap_attribution to an item whose
 * shipped_at falls in that quarter. Drives CouncilMark dot-lighting.
 *
 * Note: pulse votes do NOT light a quarter — the sigil represents consequential
 * contribution, not participation. See council-mark spec.
 */
export function getLitQuarters(userId: number, anchor: Date = new Date()): Set<number> {
  const rows = db.prepare(`
    SELECT ri.shipped_at
    FROM roadmap_attributions ra
    JOIN roadmap_items ri ON ri.id = ra.roadmap_item_id
    WHERE ra.user_id = ? AND ri.shipped_at IS NOT NULL
  `).all(userId) as { shipped_at: string }[];

  const out = new Set<number>();
  const anchorQ = Math.floor(anchor.getUTCMonth() / 3);
  const anchorYear = anchor.getUTCFullYear();

  for (const row of rows) {
    const shipped = parseSqlDate(row.shipped_at);
    const sQ = Math.floor(shipped.getUTCMonth() / 3);
    const sYear = shipped.getUTCFullYear();
    const back = (anchorYear - sYear) * 4 + (anchorQ - sQ);
    if (back >= 0 && back < 12) out.add(back);
  }
  return out;
}

export function countShippedAttributions(userId: number): number {
  const r = db.prepare(`
    SELECT COUNT(*) AS n
    FROM roadmap_attributions ra
    JOIN roadmap_items ri ON ri.id = ra.roadmap_item_id
    WHERE ra.user_id = ? AND ri.shipped_at IS NOT NULL
  `).get(userId) as { n: number };
  return r.n;
}

// ── Dispatches ───────────────────────────────────────────────────

export type DispatchKind = 'decision' | 'update' | 'behind_the_scenes' | 'note';
export type DispatchStatus = 'draft' | 'published' | 'archived';

export interface Dispatch {
  id: number;
  title: string;
  body: string;
  excerpt: string | null;
  kind: DispatchKind;
  author_id: number;
  status: DispatchStatus;
  published_at: string | null;
  created_at: string;
  updated_at: string;
}

export interface DispatchWithAuthor extends Dispatch {
  author_name: string;
  author_title: string | null;
  author_role: Role;
}

export function createDispatch(data: {
  title: string;
  body: string;
  excerpt: string | null;
  kind: DispatchKind;
  author_id: number;
  status: DispatchStatus;
}): number {
  const published_at = data.status === 'published'
    ? new Date().toISOString().slice(0, 19).replace('T', ' ')
    : null;
  const r = db.prepare(`
    INSERT INTO dispatches (title, body, excerpt, kind, author_id, status, published_at)
    VALUES (?,?,?,?,?,?,?)
  `).run(data.title, data.body, data.excerpt, data.kind, data.author_id, data.status, published_at);
  return Number(r.lastInsertRowid);
}

export function updateDispatch(id: number, data: {
  title: string;
  body: string;
  excerpt: string | null;
  kind: DispatchKind;
  author_id: number;
}): void {
  db.prepare(`
    UPDATE dispatches
       SET title = ?, body = ?, excerpt = ?, kind = ?, author_id = ?, updated_at = datetime('now')
     WHERE id = ?
  `).run(data.title, data.body, data.excerpt, data.kind, data.author_id, id);
}

/** Promote draft → published, stamping published_at = now() on first publish.
 *  Idempotent: if already published, published_at is preserved. */
export function publishDispatch(id: number): void {
  db.prepare(`
    UPDATE dispatches
       SET status = 'published',
           published_at = COALESCE(published_at, datetime('now')),
           updated_at = datetime('now')
     WHERE id = ?
  `).run(id);
}

/** Archive a dispatch. Leaves published_at intact for history. */
export function archiveDispatch(id: number): void {
  db.prepare("UPDATE dispatches SET status = 'archived', updated_at = datetime('now') WHERE id = ?").run(id);
}

export function deleteDispatch(id: number): void {
  db.prepare('DELETE FROM dispatches WHERE id = ?').run(id);
}

export function getDispatchById(id: number): DispatchWithAuthor | null {
  return db.prepare(`
    SELECT d.*, u.name AS author_name, u.title AS author_title, u.role AS author_role
    FROM dispatches d JOIN users u ON u.id = d.author_id
    WHERE d.id = ?
  `).get(id) as DispatchWithAuthor | null;
}

export function getLatestPublishedDispatches(limit: number): DispatchWithAuthor[] {
  return db.prepare(`
    SELECT d.*, u.name AS author_name, u.title AS author_title, u.role AS author_role
    FROM dispatches d JOIN users u ON u.id = d.author_id
    WHERE d.status = 'published'
    ORDER BY d.published_at DESC, d.id DESC
    LIMIT ?
  `).all(limit) as DispatchWithAuthor[];
}

export function getAllDispatchesForAdmin(opts: { status?: DispatchStatus } = {}): DispatchWithAuthor[] {
  const clause = opts.status ? `WHERE d.status = '${opts.status}'` : '';
  return db.prepare(`
    SELECT d.*, u.name AS author_name, u.title AS author_title, u.role AS author_role
    FROM dispatches d JOIN users u ON u.id = d.author_id
    ${clause}
    ORDER BY d.created_at DESC, d.id DESC
  `).all() as DispatchWithAuthor[];
}

/** Adjacent dispatches in published order. Returns the immediately newer and older. */
export function getAdjacentDispatches(id: number): { prev: DispatchWithAuthor | null; next: DispatchWithAuthor | null } {
  const cur = getDispatchById(id);
  if (!cur || cur.status !== 'published' || !cur.published_at) return { prev: null, next: null };
  const prev = db.prepare(`
    SELECT d.*, u.name AS author_name, u.title AS author_title, u.role AS author_role
    FROM dispatches d JOIN users u ON u.id = d.author_id
    WHERE d.status = 'published' AND d.published_at < ?
    ORDER BY d.published_at DESC LIMIT 1
  `).get(cur.published_at) as DispatchWithAuthor | null;
  const next = db.prepare(`
    SELECT d.*, u.name AS author_name, u.title AS author_title, u.role AS author_role
    FROM dispatches d JOIN users u ON u.id = d.author_id
    WHERE d.status = 'published' AND d.published_at > ?
    ORDER BY d.published_at ASC LIMIT 1
  `).get(cur.published_at) as DispatchWithAuthor | null;
  return { prev, next };
}

// ── Event attendees (for the avatar pile on /events) ─────────────

export function getEventAttendees(slug: string, status: 'yes' | 'interested' | 'no' = 'yes'): UserPublic[] {
  return db.prepare(`
    SELECT u.id, u.email, u.name, u.organisation, u.role, u.bio, u.created_at, u.last_seen_at, u.active,
           u.title, u.cab_joined_date, u.slug, u.pull_quote, u.member_number, u.focus_tags
    FROM attendance a JOIN users u ON u.id = a.user_id
    WHERE a.meeting_slug = ? AND a.kind = 'event' AND a.status = ?
    ORDER BY a.updated_at DESC
  `).all(slug, status) as UserPublic[];
}
