export * as SessionTransfer from "./transfer"

import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
import { Tool } from "@opencode-ai/schema/tool"
import { eq, isNotNull, isNull, ne, or } from "drizzle-orm"
import { Context, DateTime, Effect, Layer, Schema } from "effect"
import path from "path"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { App } from "../app"
import { Bus } from "../bus"
import { Database } from "../database/database"
import { Location } from "../location"
import { Project } from "../project"
import { ProjectTable } from "../project/sql"
import { AbsolutePath, RelativePath } from "../schema"
import { Session } from "../session"
import { Slug } from "../util/slug"
import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
import { SessionProjector } from "./projector"
import { SessionMessageTable, SessionTable } from "./sql"

export const Data = SessionTransfer.Data
export type Data = SessionTransfer.Data

export class ImportConflictError extends Schema.TaggedErrorClass<ImportConflictError>()(
  "SessionTransfer.ImportConflictError",
  { sessionID: Session.ID },
) {}

export interface Interface {
  readonly export: (input: {
    sessionID: Session.ID
    sanitize?: boolean
  }) => Effect.Effect<Data, Session.NotFoundError | Session.MessageDecodeError>
  readonly import: (input: {
    data: Data
    location: Location.Ref
  }) => Effect.Effect<Session.Info, ImportConflictError>
}

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

const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const app = yield* App.Metadata
    const bus = yield* Bus.Service
    const { db } = yield* Database.Service
    const projects = yield* Project.Service
    const sessions = yield* Session.Service
    const encodeMessage = Schema.encodeSync(SessionMessage.Info)

    const persistProject = (project: Project.Resolved) => {
      const vcs = project.vcs?.type
      return db
        .insert(ProjectTable)
        .values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] })
        .onConflictDoUpdate({
          target: ProjectTable.id,
          set: { worktree: project.canonical, vcs: vcs ?? null },
          setWhere: or(
            ne(ProjectTable.worktree, project.canonical),
            vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs),
          ),
        })
        .run()
        .pipe(Effect.orDie)
    }

    return Service.of({
      export: Effect.fn("SessionTransfer.export")(function* (input) {
        const data = {
          info: yield* sessions.get(input.sessionID),
          messages: yield* sessions.messages({ sessionID: input.sessionID, order: "asc" }),
        }
        return input.sanitize ? sanitize(data) : data
      }),
      import: Effect.fn("SessionTransfer.import")(function* (input) {
        const sessionID = input.data.info.id
        const recorded = yield* db
          .select({ id: SessionTable.id })
          .from(SessionTable)
          .where(eq(SessionTable.id, sessionID))
          .get()
          .pipe(Effect.orDie)
        if (recorded) return yield* new ImportConflictError({ sessionID })
        const project = yield* projects.resolve(input.location.directory)
        yield* persistProject(project)
        const messages = input.data.messages.map((message, index) => {
          const encoded = encodeMessage(message)
          const { id: _, type, ...data } = encoded
          return {
            id: message.id,
            session_id: sessionID,
            type,
            seq: index + 1,
            time_created: DateTime.toEpochMillis(message.time.created),
            data,
          }
        })
        yield* bus
          .publish(
            SessionEvent.Created,
            {
              sessionID,
              slug: Slug.create(),
              version: app.version,
              projectID: project.id,
              location: input.location,
              subpath: RelativePath.make(path.relative(project.directory, input.location.directory).replaceAll("\\", "/")),
              title: input.data.info.title,
              agent: input.data.info.agent,
              model: input.data.info.model,
            },
            {
              location: input.location,
              commit: (seq) =>
                Effect.gen(function* () {
                  if (messages.length > 0) {
                    yield* db.insert(SessionMessageTable).values(messages).run().pipe(Effect.orDie)
                    yield* Bus.reserveSequence(db, sessionID, seq + messages.length)
                  }
                  yield* db
                    .update(SessionTable)
                    .set({
                      cost: input.data.info.cost,
                      tokens_input: input.data.info.tokens.input,
                      tokens_output: input.data.info.tokens.output,
                      tokens_reasoning: input.data.info.tokens.reasoning,
                      tokens_cache_read: input.data.info.tokens.cache.read,
                      tokens_cache_write: input.data.info.tokens.cache.write,
                      time_created: DateTime.toEpochMillis(input.data.info.time.created),
                      time_updated: DateTime.toEpochMillis(input.data.info.time.updated),
                      time_archived: input.data.info.time.archived
                        ? DateTime.toEpochMillis(input.data.info.time.archived)
                        : null,
                    })
                    .where(eq(SessionTable.id, sessionID))
                    .run()
                    .pipe(Effect.orDie)
                }),
            },
          )
          .pipe(
            Effect.catchDefect((defect) =>
              defect instanceof SessionProjector.SessionAlreadyProjected
                ? Effect.fail(new ImportConflictError({ sessionID }))
                : Effect.die(defect),
            ),
          )
        return yield* sessions.get(sessionID).pipe(Effect.orDie)
      }),
    })
  }),
)

export const node = makeGlobalNode({
  service: Service,
  layer,
  deps: [App.node, Bus.node, Database.node, Project.node, Session.node],
})

function redact(kind: string, id: string, value: string) {
  return value.trim() ? `[redacted:${kind}:${id}]` : value
}

function metadata(kind: string, id: string, value: Readonly<Record<string, unknown>> | undefined) {
  if (!value) return value
  return Object.keys(value).length > 0 ? { redacted: `${kind}:${id}` } : value
}

function sanitize(data: Data): Data {
  return {
    info: {
      ...data.info,
      title: data.info.title === undefined ? undefined : redact("session-title", data.info.id, data.info.title),
      location: {
        ...data.info.location,
        directory: AbsolutePath.make(`/${redact("session-directory", data.info.id, data.info.location.directory)}`),
      },
      revert: data.info.revert
        ? {
            ...data.info.revert,
            files: data.info.revert.files?.map((file, index) => ({
              ...file,
              file: redact("revert-file", String(index), file.file),
              patch: redact("revert-patch", String(index), file.patch),
            })),
          }
        : undefined,
    },
    messages: data.messages.map(sanitizeMessage),
  }
}

function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
  const meta = metadata("message-metadata", message.id, message.metadata)
  if (message.type === "user")
    return {
      ...message,
      metadata: meta,
      text: redact("text", message.id, message.text),
      files: message.files?.map((file, index) => ({
        ...file,
        data: "",
        source: { type: "inline" },
        name: file.name === undefined ? undefined : redact("file-name", String(index), file.name),
        description:
          file.description === undefined ? undefined : redact("file-description", String(index), file.description),
        mention: file.mention
          ? { ...file.mention, text: redact("file-mention", String(index), file.mention.text) }
          : undefined,
      })),
      agents: message.agents?.map((agent, index) => ({
        ...agent,
        name: redact("agent-name", String(index), agent.name),
        mention: agent.mention
          ? { ...agent.mention, text: redact("agent-mention", String(index), agent.mention.text) }
          : undefined,
      })),
    }
  if (message.type === "synthetic")
    return {
      ...message,
      metadata: meta,
      text: redact("synthetic", message.id, message.text),
      description:
        message.description === undefined
          ? undefined
          : redact("synthetic-description", message.id, message.description),
    }
  if (message.type === "system")
    return { ...message, metadata: meta, text: redact("system", message.id, message.text) }
  if (message.type === "skill") return { ...message, metadata: meta, text: redact("skill", message.id, message.text) }
  if (message.type === "shell")
    return {
      ...message,
      metadata: meta,
      command: redact("shell-command", message.id, message.command),
      output: message.output
        ? { ...message.output, output: redact("shell-output", message.id, message.output.output) }
        : undefined,
    }
  if (message.type === "assistant")
    return {
      ...message,
      metadata: meta,
      content: message.content.map((content) => {
        if (content.type === "text")
          return {
            ...content,
            text: redact("text", message.id, content.text),
            state: content.state ? { redacted: `text-state:${message.id}` } : undefined,
          }
        if (content.type === "reasoning")
          return {
            ...content,
            text: redact("reasoning", message.id, content.text),
            state: content.state ? { redacted: `reasoning-state:${message.id}` } : undefined,
          }
        return {
          ...content,
          providerState: content.providerState ? { redacted: `tool-provider-state:${message.id}` } : undefined,
          providerResultState: content.providerResultState
            ? { redacted: `tool-provider-result-state:${message.id}` }
            : undefined,
          state: sanitizeToolState(message.id, content.state),
        }
      }),
    }
  if (message.type === "compaction") {
    if (message.status === "failed")
      return {
        ...message,
        metadata: meta,
      }
    return {
      ...message,
      metadata: meta,
      summary: redact("compaction-summary", message.id, message.summary),
      recent: redact("compaction-recent", message.id, message.recent),
    }
  }
  return { ...message, metadata: meta }
}

function sanitizeToolState(id: string, state: SessionMessage.ToolState): SessionMessage.ToolState {
  if (state.status === "streaming") return { ...state, input: redact("tool-input", id, state.input) }
  if (state.status === "running")
    return { ...state, input: { redacted: `tool-input:${id}` }, metadata: { redacted: `tool-metadata:${id}` } }
  const meta = state.metadata === undefined ? undefined : { redacted: `tool-metadata:${id}` }
  if (state.status === "completed")
    return {
      ...state,
      input: { redacted: `tool-input:${id}` },
      content: [
        sanitizeToolContent(id, state.content[0]),
        ...state.content.slice(1).map((item) => sanitizeToolContent(id, item)),
      ],
      metadata: meta,
    }
  return {
    ...state,
    input: { redacted: `tool-input:${id}` },
    content: state.content
      ? [
          sanitizeToolContent(id, state.content[0]),
          ...state.content.slice(1).map((item) => sanitizeToolContent(id, item)),
        ]
      : undefined,
    metadata: meta,
  }
}

function sanitizeToolContent(id: string, content: Tool.Content): Tool.Content {
  if (content.type === "text") return { ...content, text: redact("tool-output", id, content.text) }
  return {
    ...content,
    uri: redact("tool-file-uri", id, content.uri),
    name: content.name === undefined ? undefined : redact("tool-file-name", id, content.name),
  }
}
