import { sql } from "@/lib/db/client"
import { tierOf } from "@/lib/db/schema"
import { markMissedAsOf } from "@/lib/services/generation"
import { dispatchPending } from "@/lib/services/notifications"
import { recordEvent } from "@/lib/services/events"
import { db } from "@/lib/db/client"
import {
  approve,
  finish,
  remind,
  sendBack,
  start,
} from "@/lib/services/task-commands"
import type { SignedIn } from "@/lib/services/session"

/**
 * Measuring the notification half against the real database, and putting every
 * row back.
 *
 *     bun run tmp/notif/measure.ts
 *
 * Throwaway. Every task row it touches is snapshotted first and restored in the
 * `finally`, and the events and notifications it creates are deleted: they name
 * real people and leaving them would put invented notifications in front of
 * them.
 */

const TASK_A = "f6e65795-697c-4074-8395-ec6dea6de552" // Accounting, assignee ≠ checker
const TASK_B = "9d202497-a4bf-4c06-97e1-adaeaa9b1bbc" // the assignee is also the checker
const ADMIN = "169784a2-a673-49c0-82d8-38886173bbd4"

const touched = [TASK_A, TASK_B]

function line(label: string, value: unknown) {
  console.log(`  ${label}: ${JSON.stringify(value)}`)
}

async function asUser(personId: string): Promise<SignedIn> {
  const [row] = await sql`
    select pe.id, pe.employee_code, pe.first_name, pe.last_name, pe.language,
           pos.title, pos.level, d.name as department
    from people pe
    left join positions pos on pos.id = pe.position_id
    left join departments d on d.id = pos.department_id
    where pe.id = ${personId}`
  return {
    personId: row!.id,
    employeeCode: row!.employee_code,
    name: `${row!.first_name} ${row!.last_name ?? ""}`.trim(),
    positionTitle: row!.title,
    department: row!.department,
    level: row!.level,
    tier: row!.level ? tierOf(row!.level) : "staff",
    language: row!.language,
  }
}

/** Who a task names, read straight from the tables rather than from a service. */
async function people(taskId: string) {
  const [row] = await sql`
    select ti.assignee_person_id as assignee, rt.checker_person_id as checker
    from task_instances ti
    join recurring_templates rt on rt.id = ti.recurring_template_id
    where ti.id = ${taskId}`
  return { assignee: row!.assignee as string, checker: row!.checker as string }
}

/** Every notification raised by an event, straight from the table. */
async function notifiedBy(taskId: string, type: string) {
  return sql`
    select n.person_id, n.message_key, n.task_instance_id, n.read_at
    from notifications n
    join domain_events e on e.id = n.event_id
    where e.task_instance_id = ${taskId} and e.type = ${type}`
}

async function eventsFor(taskId: string) {
  return sql`select id, type, actor_person_id, payload, dispatched_at
             from domain_events where task_instance_id = ${taskId} order by occurred_at`
}

const before = {
  tasks: await sql`select * from task_instances where id = any(${touched})`,
  statuses: await sql`select status, count(*) from task_instances group by status order by status`,
  submissions: await sql`select count(*) from task_submissions`,
  events: await sql`select count(*) from domain_events`,
  notifications: await sql`select count(*) from notifications`,
}

console.log("BEFORE")
line("statuses", before.statuses)
line("submissions", before.submissions[0])
line("events", before.events[0])
line("notifications", before.notifications[0])

/** Rows the missed sweep may move, so they can be moved back exactly. */
const openBefore = await sql`
  select id, status from task_instances where status in ('todo', 'started')`

try {
  const { assignee, checker } = await people(TASK_A)
  const worker = await asUser(assignee)
  const boss = await asUser(checker)
  const admin = await asUser(ADMIN)
  console.log(`\nTask A ${TASK_A}: assignee ${assignee}, checker ${checker}`)

  /* 1 — a manager reminds ------------------------------------------------- */
  console.log("\n1. A reminder, sent by an admin who is neither of them")
  line("remind", await remind(admin, TASK_A, "Before the line starts, please."))
  line("notified", await notifiedBy(TASK_A, "task.reminded"))
  line("expected recipient (the assignee)", assignee)

  /* 2 — finishing --------------------------------------------------------- */
  console.log("\n2. Finishing, which somebody is waiting on")
  line("start", await start(worker, TASK_A))
  line("finish", await finish(worker, TASK_A, "Counted and filed."))
  line("notified", await notifiedBy(TASK_A, "task.finished"))
  line("expected recipient (the checker)", checker)

  /* 3 — sending back ------------------------------------------------------ */
  console.log("\n3. Sent back, with the note that says why")
  line("sendBack", await sendBack(boss, TASK_A, "The weights are last shift's."))
  line("notified", await notifiedBy(TASK_A, "task.sent_back"))
  line("expected recipient (the assignee)", assignee)

  /* 4 — approving --------------------------------------------------------- */
  console.log("\n4. Approved, which closes the loop")
  line("finish again", await finish(worker, TASK_A, "Recounted."))
  line("approve", await approve(boss, TASK_A))
  line("notified", await notifiedBy(TASK_A, "task.checked"))
  line("expected recipient (the assignee)", assignee)

  console.log("\nEvery event on task A, and who each reached")
  line("events", await eventsFor(TASK_A))
  line(
    "notifications on task A",
    await sql`select person_id, message_key from notifications where task_instance_id = ${TASK_A}`,
  )

  /* 5 — nobody notifies themselves ---------------------------------------- */
  console.log("\n5. The assignee is also the checker, so finishing tells nobody")
  const b = await people(TASK_B)
  const solo = await asUser(b.assignee)
  line("assignee === checker", b.assignee === b.checker)
  line("start", await start(solo, TASK_B))
  line("finish", await finish(solo, TASK_B, "Done."))
  line("events", await eventsFor(TASK_B))
  line(
    "notifications",
    await sql`select * from notifications where task_instance_id = ${TASK_B}`,
  )

  /* 6 — the sweep records and tells nobody -------------------------------- */
  console.log("\n6. Marking missed: events written, nobody told")
  // Nothing is past its cut-off as of today, so the sweep is asked about a
  // later day: the same code, on the same rows, with the clock moved rather
  // than the data. Everything it marks is put back from `openBefore`.
  const swept = await markMissedAsOf("2026-08-07")
  line("markMissedAsOf 2026-08-07", swept)
  const missedEvents = await sql`
    select count(*) from domain_events where type = 'task.missed'`
  line("task.missed events", missedEvents[0])
  line(
    "notifications from them",
    (
      await sql`select count(*) from notifications n
                join domain_events e on e.id = n.event_id
                where e.type = 'task.missed'`
    )[0],
  )

  /* 7 — the drain, twice --------------------------------------------------- */
  console.log("\n7. The drain, run twice, counted both times")
  // Every event above was dispatched at commit. Undoing the stamp is what a
  // crash between sending and stamping leaves behind, which is the case the
  // drain has to survive.
  await sql`update domain_events set dispatched_at = null`
  const countRow = async () =>
    Number(
      (await sql`select count(*)::int as n from notifications`)[0]!.n as number,
    )
  line("notifications before the drain", await countRow())
  line("first drain", await dispatchPending())
  const afterFirst = await countRow()
  line("notifications after the first", afterFirst)
  line("second drain", await dispatchPending())
  const afterSecond = await countRow()
  line("notifications after the second", afterSecond)
  line("doubled?", afterSecond !== afterFirst)

  /* 8 — a rolled-back change leaves no event ------------------------------- */
  console.log("\n8. A change that rolls back takes its event with it")
  const eventsBefore = await eventsFor(TASK_B)
  try {
    await db.transaction(async (tx) => {
      await recordEvent(tx, {
        type: "task.checked",
        taskInstanceId: TASK_B,
        actorPersonId: ADMIN,
      })
      throw new Error("the change failed after the event was recorded")
    })
  } catch (error) {
    line("the transaction threw", (error as Error).message)
  }
  const eventsAfter = await eventsFor(TASK_B)
  line("events before", eventsBefore.length)
  line("events after", eventsAfter.length)

  /* 9 — what a person would read ------------------------------------------ */
  console.log("\n9. The feed, as the assignee of task A would read it")
  line(
    "rows",
    await sql`
      select n.message_key, n.read_at is null as unread, e.payload,
             (select first_name from people p where p.id = e.actor_person_id) as actor
      from notifications n join domain_events e on e.id = n.event_id
      where n.person_id = ${assignee}
      order by (n.read_at is null) desc, n.created_at desc`,
  )
} finally {
  console.log("\nPUTTING EVERY ROW BACK")

  await sql`delete from notifications`
  await sql`delete from domain_events`
  await sql`delete from task_submissions where task_instance_id = any(${touched})`

  for (const row of before.tasks) {
    await sql`
      update task_instances set
        status = ${row.status},
        started_at = ${row.started_at},
        finished_at = ${row.finished_at},
        checked_at = ${row.checked_at},
        checked_by = ${row.checked_by},
        verified_at = ${row.verified_at},
        verified_by = ${row.verified_by},
        sent_back_at = ${row.sent_back_at},
        sent_back_by = ${row.sent_back_by},
        sent_back_note = ${row.sent_back_note}
      where id = ${row.id}`
  }

  // Whatever the sweep moved, moved back to the status it held.
  for (const row of openBefore) {
    await sql`update task_instances set status = ${row.status}
              where id = ${row.id} and status <> ${row.status}`
  }

  console.log("AFTER")
  line(
    "statuses",
    await sql`select status, count(*) from task_instances group by status order by status`,
  )
  line("submissions", (await sql`select count(*) from task_submissions`)[0])
  line("events", (await sql`select count(*) from domain_events`)[0])
  line("notifications", (await sql`select count(*) from notifications`)[0])
  line(
    "the two tasks",
    await sql`select id, status, started_at, finished_at, checked_at, sent_back_at
              from task_instances where id = any(${touched})`,
  )
  await sql.end()
}
