import path from "path"
import { Decimal } from "decimal.js"
import z from "zod"
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
import { InstallationVersion } from "@saeeol/core/installation/version"
import { BusEvent } from "@/bus/bus-event"
import { SyncEvent } from "../../sync"
import { PartTable, SessionTable } from "./session.sql"
import * as Log from "@saeeol/core/util/log"
import { MessageV2 } from "../message/message-v2"
import { Snapshot } from "@/snapshot"
import { ProjectID } from "../../project/schema"
import { WorkspaceID } from "../../control-plane/schema"
import { SessionID, MessageID, PartID } from "./schema"
import type { Provider } from "@/provider/provider"
import { Permission } from "@/permission"
import { Global } from "@saeeol/core/global"
import { SaeeolSession } from "@/saeeol/session"
import { Effect, Schema, Types } from "effect"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
import { SyncEvents, BusEvents } from "./session-events"

const log = Log.create({ service: "session" })

const parentTitlePrefix = "New session - "
const childTitlePrefix = "Child session - "

export function createDefaultTitle(isChild = false) {
  return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
}

export function isDefaultTitle(title: string) {
  return new RegExp(`^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`).test(title)
}

type SessionRow = typeof SessionTable.$inferSelect

export function fromRow(row: SessionRow): Info {
  const summary = row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
    ? { additions: row.summary_additions ?? 0, deletions: row.summary_deletions ?? 0, files: row.summary_files ?? 0, diffs: row.summary_diffs ?? undefined }
    : undefined
  const share = row.share_url ? { url: row.share_url } : undefined
  return {
    id: row.id, slug: row.slug, projectID: row.project_id, workspaceID: row.workspace_id ?? undefined,
    directory: row.directory, path: row.path ?? undefined, parentID: row.parent_id ?? undefined,
    title: row.title, version: row.version, summary, share, revert: row.revert ?? undefined,
    permission: row.permission ?? undefined,
    time: { created: row.time_created, updated: row.time_updated, compacting: row.time_compacting ?? undefined, archived: row.time_archived ?? undefined },
  }
}

export function toRow(info: Info) {
  return {
    id: info.id, project_id: info.projectID, workspace_id: info.workspaceID, parent_id: info.parentID,
    slug: info.slug, directory: info.directory, path: info.path, title: info.title, version: info.version,
    share_url: info.share?.url, summary_additions: info.summary?.additions, summary_deletions: info.summary?.deletions,
    summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, revert: info.revert ?? null,
    permission: info.permission, time_created: info.time.created, time_updated: info.time.updated,
    time_compacting: info.time.compacting, time_archived: info.time.archived,
  }
}

export function getForkedTitle(title: string): string {
  const match = title.match(/^(.+) \(fork #(\d+)\)$/)
  if (match) return `${match[1]} (fork #${parseInt(match[2], 10) + 1})`
  return `${title} (fork #1)`
}

export function sessionPath(worktree: string, cwd: string) {
  return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
}

export const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
const Share = Schema.Struct({ url: Schema.String })
export const ArchivedTimestamp = Schema.Finite
const Time = Schema.Struct({ created: NonNegativeInt, updated: NonNegativeInt, compacting: optionalOmitUndefined(NonNegativeInt), archived: optionalOmitUndefined(ArchivedTimestamp) })
export const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })

export const Info = Schema.Struct({
  id: SessionID, slug: Schema.String, projectID: ProjectID, workspaceID: optionalOmitUndefined(WorkspaceID),
  directory: Schema.String, path: optionalOmitUndefined(Schema.String), parentID: optionalOmitUndefined(SessionID),
  summary: optionalOmitUndefined(Summary), share: optionalOmitUndefined(Share), title: Schema.String,
  version: Schema.String, time: Time, permission: optionalOmitUndefined(Permission.Ruleset), revert: optionalOmitUndefined(Revert),
}).annotate({ identifier: "Session" }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>

export const ProjectInfo = Schema.Struct({ id: ProjectID, name: optionalOmitUndefined(Schema.String), worktree: Schema.String })
  .annotate({ identifier: "ProjectSummary" }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>

export const GlobalInfo = Schema.Struct({ ...Info.fields, project: Schema.NullOr(ProjectInfo), worktreeName: Schema.optional(Schema.String) })
  .annotate({ identifier: "GlobalSession" }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>

export const CreateInput = Schema.optional(Schema.Struct({
  parentID: Schema.optional(SessionID), title: Schema.optional(Schema.String), permission: Schema.optional(Permission.Ruleset),
  platform: Schema.optional(Schema.String), workspaceID: Schema.optional(WorkspaceID),
})).pipe(withStatics((s) => ({ zod: zod(s) })))
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>

export const ForkInput = Schema.Struct({ sessionID: SessionID, messageID: Schema.optional(MessageID) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export const GetInput = SessionID
export const ChildrenInput = SessionID
export const RemoveInput = SessionID
export const SetTitleInput = Schema.Struct({ sessionID: SessionID, title: Schema.String }).pipe(withStatics((s) => ({ zod: zod(s) })))
export const SetArchivedInput = Schema.Struct({ sessionID: SessionID, time: Schema.optional(ArchivedTimestamp) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export const SetPermissionInput = Schema.Struct({ sessionID: SessionID, permission: Permission.Ruleset }).pipe(withStatics((s) => ({ zod: zod(s) })))
export const SetRevertInput = Schema.Struct({ sessionID: SessionID, revert: Schema.optional(Revert), summary: Schema.optional(Summary) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export const MessagesInput = Schema.Struct({ sessionID: SessionID, limit: Schema.optional(NonNegativeInt) }).pipe(withStatics((s) => ({ zod: zod(s) })))

export type ListInput = { directory?: string; scope?: "project"; path?: string; workspaceID?: WorkspaceID; roots?: boolean; start?: number; search?: string; limit?: number }

const CreatedEventSchema = Schema.Struct({ sessionID: SessionID, info: Info })
const UpdatedShare = Schema.Struct({ url: Schema.optional(Schema.NullOr(Schema.String)) })
const UpdatedTime = Schema.Struct({ created: Schema.optional(Schema.NullOr(NonNegativeInt)), updated: Schema.optional(Schema.NullOr(NonNegativeInt)), compacting: Schema.optional(Schema.NullOr(NonNegativeInt)), archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)) })
const UpdatedInfo = Schema.Struct({
  id: Schema.optional(Schema.NullOr(SessionID)), slug: Schema.optional(Schema.NullOr(Schema.String)),
  projectID: Schema.optional(Schema.NullOr(ProjectID)), workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
  directory: Schema.optional(Schema.NullOr(Schema.String)), path: Schema.optional(Schema.NullOr(Schema.String)),
  parentID: Schema.optional(Schema.NullOr(SessionID)), summary: Schema.optional(Schema.NullOr(Summary)),
  share: Schema.optional(UpdatedShare), title: Schema.optional(Schema.NullOr(Schema.String)),
  version: Schema.optional(Schema.NullOr(Schema.String)), time: Schema.optional(UpdatedTime),
  permission: Schema.optional(Schema.NullOr(Permission.Ruleset)), revert: Schema.optional(Schema.NullOr(Revert)),
})
const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, info: UpdatedInfo })

export const Event = {
  ...SyncEvents,
  ...BusEvents,
  TurnOpen: SaeeolSession.Event.TurnOpen,
  TurnClose: SaeeolSession.Event.TurnClose,
}

export function plan(input: { slug: string; time: { created: number } }, instance: any) {
  const base = instance.project.vcs ? path.join(instance.worktree, ".saeeol", "plans") : path.join(Global.Path.data, "plans")
  return path.join(base, [input.time.created, input.slug].join("-") + ".md")
}

export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata; provider?: Provider.Info }) => {
  const safe = (v: number) => (Number.isFinite(v) ? v : 0)
  const inputTokens = safe(input.usage.inputTokens ?? 0)
  const outputTokens = safe(input.usage.outputTokens ?? 0)
  const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
  const cacheRead = safe(input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0)
  const cacheWrite = safe(Number(input.usage.inputTokenDetails?.cacheWriteTokens ?? input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? (input.metadata as any)?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? (input.metadata as any)?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? 0))
  const adjustedInput = safe(inputTokens - cacheRead - cacheWrite)
  const tokens = { total: input.usage.totalTokens, input: adjustedInput, output: safe(outputTokens - reasoningTokens), reasoning: reasoningTokens, cache: { write: cacheWrite, read: cacheRead } }
  const reported = SaeeolSession.providerCost({ metadata: input.metadata, usage: input.usage, provider: input.provider, providerID: input.model.providerID })
  if (reported !== undefined) return { cost: safe(reported), tokens }
  const costInfo = input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 ? input.model.cost.experimentalOver200K : input.model.cost
  return {
    cost: safe(new Decimal(0).add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)).add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)).add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)).toNumber()),
    tokens,
  }
}

export class BusyError extends Error { constructor(public readonly sessionID: string) { super(`Session ${sessionID} is busy`) } }
