import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { basename, dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { Data, Effect } from "effect"

/**
 * `efmesh schedule` (#10): registers a `run` tick in the OS scheduler via
 * Bun.cron (>=1.3.11): crontab on Linux, launchd on macOS, Task Scheduler
 * on Windows. One title = one entry; re-registration overwrites in place
 * (idempotent).
 *
 * Honest caveats (documented in README/SPEC): cron does not catch up on
 * missed runs (systemd Persistent=true is stricter — hence --print-systemd),
 * the OS level lives in the LOCAL timezone (TZ=UTC on top), on Linux a live
 * cron daemon is required — the Arch family without cronie has none at all.
 * Overlapping ticks are safe by construction: run holds the env lock, and
 * "awaiting a human" = exit 2, not a failure.
 */

export class ScheduleError extends Data.TaggedError("ScheduleError")<{
  readonly reason: string
}> {
  override get message(): string {
    return this.reason
  }
}

/** Everything the worker needs, as absolute paths (crontab has no cwd). */
export interface ScheduleTarget {
  /** Project directory (where the config lives) — the tick's cwd. */
  readonly project: string
  /** Absolute path of the config. */
  readonly config: string
  readonly env: string
}

/** Bun.cron title: only [A-Za-z0-9_-]; includes the project and environment. */
export const scheduleTitle = (target: Pick<ScheduleTarget, "project" | "env">): string =>
  `efmesh-${basename(target.project)}-${target.env}`.replaceAll(/[^A-Za-z0-9_-]/g, "-")

/** Where the worker is generated: next to the config, in the internal .efmesh/. */
export const workerPath = (target: ScheduleTarget): string =>
  join(target.project, ".efmesh", `schedule-${target.env}.ts`)

/** CLI binary of this same package — the worker calls it without guessing npm names. */
const binPath = (): string => fileURLToPath(new URL("../bin.ts", import.meta.url))

/**
 * Worker source for Bun.cron: the OS scheduler runs scheduled(), which runs
 * an ordinary `efmesh run` — the same exit-code semantics (2 = "awaiting a
 * human") and the same tick journal in the store (`efmesh status`).
 */
export const workerSource = (
  target: ScheduleTarget,
): string => `// generated by \`efmesh schedule\` — do not edit: re-registration overwrites it
export default {
  async scheduled() {
    const proc = Bun.spawn(
      [${JSON.stringify(process.execPath)}, ${JSON.stringify(binPath())}, "run", ${JSON.stringify(target.env)}, "--config", ${JSON.stringify(target.config)}],
      { cwd: ${JSON.stringify(target.project)}, stdout: "inherit", stderr: "inherit" },
    )
    process.exitCode = await proc.exited
  },
}
`

/** cron nicknames → systemd OnCalendar; arbitrary expressions are not translated. */
export const cronToOnCalendar = (cron: string): string | undefined =>
  (
    ({
      "@hourly": "hourly",
      "@daily": "daily",
      "@midnight": "daily",
      "@weekly": "weekly",
      "@monthly": "monthly",
      "@yearly": "yearly",
      "@annually": "yearly",
    }) as Record<string, string>
  )[cron.trim()]

/**
 * systemd fallback (--print-systemd): cron has no catch-up for missed runs
 * and the Arch family has no daemon — a user timer with Persistent=true is
 * more honest.
 */
export const systemdUnits = (
  target: ScheduleTarget,
  cron: string,
): { readonly service: string; readonly timer: string; readonly name: string } => {
  const name = scheduleTitle(target)
  const calendar = cronToOnCalendar(cron)
  return {
    name,
    service: `[Unit]
Description=efmesh run ${target.env} (${basename(target.project)})

[Service]
Type=oneshot
WorkingDirectory=${target.project}
ExecStart=${process.execPath} ${binPath()} run ${target.env} --config ${target.config}
# exit 2 = "awaiting a human" (apply needed) — deliberately a unit failure: visible in alerts
`,
    timer: `[Unit]
Description=efmesh run ${target.env} — timer

[Timer]
OnCalendar=${calendar ?? `hourly  # TODO: translate "${cron}" into OnCalendar by hand`}
Persistent=true

[Install]
WantedBy=timers.target
`,
  }
}

/** Validate the expression with the same parser that will execute it. */
export const validateCron = (cron: string): Effect.Effect<void, ScheduleError> =>
  Effect.gen(function* () {
    const next = yield* Effect.try({
      try: () => Bun.cron.parse(cron),
      catch: () => new ScheduleError({ reason: `cannot parse cron expression "${cron}"` }),
    })
    if (next === null) {
      return yield* new ScheduleError({
        reason: `cron expression "${cron}" will never fire`,
      })
    }
  })

/** Linux without a crontab binary (Arch family) — an honest error with a recipe. */
const requireCrontab = (): Effect.Effect<void, ScheduleError> =>
  Effect.gen(function* () {
    if (process.platform !== "linux" || Bun.which("crontab") !== null) return
    return yield* new ScheduleError({
      reason:
        "this machine has no crontab (the Arch family ships no cron daemon): " +
        "install cronie OR use a systemd timer — efmesh schedule <env> --print-systemd",
    })
  })

export const registerSchedule = (
  target: ScheduleTarget,
  cron: string,
): Effect.Effect<{ readonly title: string; readonly worker: string }, ScheduleError> =>
  Effect.gen(function* () {
    yield* validateCron(cron)
    yield* requireCrontab()
    if (!existsSync(target.config)) {
      return yield* new ScheduleError({ reason: `config not found: ${target.config}` })
    }
    const worker = workerPath(target)
    yield* Effect.try({
      try: () => {
        mkdirSync(dirname(worker), { recursive: true })
        writeFileSync(worker, workerSource(target))
      },
      catch: (cause) => new ScheduleError({ reason: `worker was not written: ${String(cause)}` }),
    })
    const title = scheduleTitle(target)
    yield* Effect.tryPromise({
      try: () => Bun.cron(worker, cron, title),
      catch: (cause) =>
        new ScheduleError({ reason: `Bun.cron did not register: ${String(cause)}` }),
    })
    return { title, worker }
  })

export const removeSchedule = (
  target: ScheduleTarget,
): Effect.Effect<{ readonly title: string }, ScheduleError> =>
  Effect.gen(function* () {
    const title = scheduleTitle(target)
    yield* Effect.tryPromise({
      try: () => Bun.cron.remove(title),
      catch: (cause) => new ScheduleError({ reason: `Bun.cron.remove: ${String(cause)}` }),
    })
    // the worker is no longer executed by anyone — clean it up silently
    yield* Effect.sync(() => rmSync(workerPath(target), { force: true }))
    return { title }
  })

/**
 * List of efmesh registrations — by Bun.cron's platform traces: `# bun-cron:
 * <title>` markers in crontab (Linux) / launchd plists (macOS).
 */
export const listSchedules = (): Effect.Effect<ReadonlyArray<string>, ScheduleError> =>
  Effect.gen(function* () {
    if (process.platform === "linux") {
      if (Bun.which("crontab") === null) return []
      const out = yield* Effect.tryPromise({
        try: async () => {
          const proc = Bun.spawn(["crontab", "-l"], { stdout: "pipe", stderr: "ignore" })
          const text = await proc.stdout.text()
          await proc.exited // an empty crontab exits with 1 — that is not an error
          return text
        },
        catch: (cause) => new ScheduleError({ reason: `crontab -l: ${String(cause)}` }),
      })
      return out.split("\n").filter((line) => line.includes("bun-cron") && line.includes("efmesh-"))
    }
    if (process.platform === "darwin") {
      const out = yield* Effect.tryPromise({
        try: async () => {
          const proc = Bun.spawn(["launchctl", "list"], { stdout: "pipe", stderr: "ignore" })
          const text = await proc.stdout.text()
          await proc.exited
          return text
        },
        catch: (cause) => new ScheduleError({ reason: `launchctl list: ${String(cause)}` }),
      })
      return out.split("\n").filter((line) => line.includes("bun.cron.efmesh-"))
    }
    return yield* new ScheduleError({
      reason: `list on ${process.platform} via the OS tools (Task Scheduler: schtasks /query)`,
    })
  })
