import { Context, Effect, Schema } from "effect"
import { Database } from "@/storage/db"
import { Project } from "@/project/project"
import { BusEvent } from "@/bus/bus-event"
import { Auth } from "@/auth"
import { EventSequenceTable } from "@/sync/event.sql"
import * as Log from "@saeeol/core/util/log"
import { ProjectID } from "@/project/schema"
import { WorkspaceTable } from "./workspace.sql"
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
import { WorkspaceID } from "./schema"
import { SessionTable } from "@/session/session.sql"
import { SessionID } from "@/session/schema"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod as effectZod, zodObject } from "@/util/effect-zod"
import { inArray } from "drizzle-orm"

export const Info = WorkspaceInfoSchema
export type Info = WorkspaceInfo

export const ConnectionStatus = Schema.Struct({ workspaceID: WorkspaceID, status: Schema.Literals(["connected", "connecting", "disconnected", "error"]) })
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>

const Restore = Schema.Struct({ workspaceID: WorkspaceID, sessionID: SessionID, total: NonNegativeInt, step: NonNegativeInt })

export const Event = {
  Ready: BusEvent.define("workspace.ready", Schema.Struct({ name: Schema.String })),
  Failed: BusEvent.define("workspace.failed", Schema.Struct({ message: Schema.String })),
  Restore: BusEvent.define("workspace.restore", Restore),
  Status: BusEvent.define("workspace.status", ConnectionStatus),
}

export function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
  return { id: row.id, type: row.type, branch: row.branch, name: row.name, directory: row.directory, extra: row.extra, projectID: row.project_id }
}

export const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
  Effect.sync(() => Database.use(fn))

export const log = Log.create({ service: "workspace-sync" })

export const CreateInput = Schema.Struct({ id: Schema.optional(WorkspaceID), type: Info.fields.type, branch: Info.fields.branch, projectID: ProjectID, extra: Info.fields.extra }).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
export type CreateInput = Schema.Schema.Type<typeof CreateInput>

export const SessionRestoreInput = Schema.Struct({ workspaceID: WorkspaceID, sessionID: SessionID }).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
export type SessionRestoreInput = Schema.Schema.Type<typeof SessionRestoreInput>

export class SyncHttpError extends Schema.TaggedErrorClass<SyncHttpError>()("WorkspaceSyncHttpError", { message: Schema.String, status: Schema.Number, body: Schema.optional(Schema.String) }) {}
export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNotFoundError>()("WorkspaceNotFoundError", { message: Schema.String, workspaceID: WorkspaceID }) {}
export class SessionEventsNotFoundError extends Schema.TaggedErrorClass<SessionEventsNotFoundError>()("WorkspaceSessionEventsNotFoundError", { message: Schema.String, sessionID: SessionID }) {}
export class SessionRestoreHttpError extends Schema.TaggedErrorClass<SessionRestoreHttpError>()("WorkspaceSessionRestoreHttpError", { message: Schema.String, workspaceID: WorkspaceID, sessionID: SessionID, status: Schema.Number, body: Schema.String }) {}
export class SyncTimeoutError extends Schema.TaggedErrorClass<SyncTimeoutError>()("WorkspaceSyncTimeoutError", { message: Schema.String, state: Schema.Record(Schema.String, Schema.Number) }) {}
export class SyncAbortedError extends Schema.TaggedErrorClass<SyncAbortedError>()("WorkspaceSyncAbortedError", { message: Schema.String, cause: Schema.optional(Schema.Defect) }) {}

type CreateError = Auth.AuthError
type SessionRestoreError = WorkspaceNotFoundError | SessionEventsNotFoundError | SessionRestoreHttpError | (any)
type WaitForSyncError = SyncTimeoutError | SyncAbortedError
type SyncLoopError = SyncHttpError | (any)

export interface Interface {
  readonly create: (input: CreateInput) => Effect.Effect<Info, CreateError>
  readonly sessionRestore: (input: SessionRestoreInput) => Effect.Effect<{ total: number }, SessionRestoreError>
  readonly list: (project: Project.Info) => Effect.Effect<Info[]>
  readonly get: (id: WorkspaceID) => Effect.Effect<Info | undefined>
  readonly remove: (id: WorkspaceID) => Effect.Effect<Info | undefined>
  readonly status: () => Effect.Effect<ConnectionStatus[]>
  readonly isSyncing: (workspaceID: WorkspaceID) => Effect.Effect<boolean>
  readonly waitForSync: (workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) => Effect.Effect<void, WaitForSyncError>
  readonly startWorkspaceSyncing: (projectID: ProjectID) => Effect.Effect<void>
}

export class Service extends Context.Service<Service, Interface>()("@saeeol/Workspace") {}

export const TIMEOUT = 5000
type HistoryEvent = { id: string; aggregate_id: string; seq: number; type: string; data: Record<string, unknown> }

export function synced(state: Record<string, number>) {
  const ids = Object.keys(state)
  if (ids.length === 0) return true
  const done = Object.fromEntries(Database.use((db) => db.select({ id: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq }).from(EventSequenceTable).where(inArray(EventSequenceTable.aggregate_id, ids)).all()).map((row) => [row.id, row.seq])) as Record<string, number>
  return ids.every((id) => (done[id] ?? -1) >= state[id])
}

export function route(url: string | URL, path: string) {
  const next = new URL(url)
  next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
  next.search = ""
  next.hash = ""
  return next
}
