export * as ProjectCopy from "./copy"

import { Context, Effect, Layer, Schema } from "effect"
import path from "path"
import { AbsolutePath } from "../schema"
import { FSUtil } from "../fs-util"
import { Git } from "../git"
import { makeLocationNode } from "../effect/app-node"
import { Project } from "../project"
import { ProjectDirectories } from "./directories"
import { makeGitWorktreeStrategy } from "./copy-strategies"
import { Slug } from "../util/slug"
import { EventV2 } from "../event"
import { Database } from "../database/database"
import { Location } from "../location"
import { Event } from "@opencode-ai/schema/project-directories"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"

export const StrategyID = ProjectCopy.StrategyID
export type StrategyID = typeof StrategyID.Type

export const CreateInput = ProjectCopy.CreateInput
export type CreateInput = typeof CreateInput.Type

export const RemoveInput = ProjectCopy.RemoveInput
export type RemoveInput = typeof RemoveInput.Type

export const RefreshInput = Schema.Struct({
  projectID: Project.ID,
}).annotate({ identifier: "ProjectCopy.RefreshInput" })
export type RefreshInput = typeof RefreshInput.Type

export const RefreshResult = Schema.Struct({
  updated: Schema.Array(AbsolutePath),
  removed: Schema.Array(AbsolutePath),
}).annotate({ identifier: "ProjectCopy.RefreshResult" })
export type RefreshResult = typeof RefreshResult.Type

export const Copy = ProjectCopy.Copy
export type Copy = typeof Copy.Type

export const ListEntry = Schema.Struct({
  directory: AbsolutePath,
  type: Schema.Literals(["root", "copy"]),
}).annotate({ identifier: "ProjectCopy.ListEntry" })
export type ListEntry = typeof ListEntry.Type

export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
  "ProjectCopy.SourceDirectoryNotFoundError",
  { directory: AbsolutePath },
) {}

export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
  "ProjectCopy.DestinationExistsError",
  { directory: AbsolutePath },
) {}

export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
  "ProjectCopy.DirectoryUnavailableError",
  { directory: AbsolutePath },
) {}

export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
  "ProjectCopy.InvalidDirectoryError",
  { directory: AbsolutePath },
) {}

export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
  "ProjectCopy.StrategyUnavailableError",
  { strategy: StrategyID },
) {}

export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
  "ProjectCopy.DuplicateStrategyError",
  { strategy: StrategyID },
) {}

export type Error =
  | SourceDirectoryNotFoundError
  | DestinationExistsError
  | DirectoryUnavailableError
  | InvalidDirectoryError
  | StrategyUnavailableError
  | Git.WorktreeError

export interface Strategy {
  readonly id: StrategyID
  readonly create: (input: {
    sourceDirectory: AbsolutePath
    directory: AbsolutePath
  }) => Effect.Effect<Copy, Git.WorktreeError | DirectoryUnavailableError>
  readonly remove: (input: {
    directory: AbsolutePath
    force: boolean
  }) => Effect.Effect<void, Git.WorktreeError | DirectoryUnavailableError>
  readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
}

export { Event }

export interface Interface {
  readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
  readonly create: (input: CreateInput) => Effect.Effect<Copy, Error>
  readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
  readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error>
}

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

export const refreshAfterBoot = Effect.gen(function* () {
  const location = yield* Location.Service
  const copies = yield* Service
  yield* Effect.gen(function* () {
    yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
    const result = yield* copies.refresh({ projectID: location.project.id })
    yield* Effect.logInfo("project copy refresh done", {
      projectID: location.project.id,
      updated: result.updated,
      removed: result.removed,
    })
  }).pipe(
    Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })),
    Effect.forkScoped,
    Effect.asVoid,
  )
})

const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const fs = yield* FSUtil.Service
    const git = yield* Git.Service
    const directories = yield* ProjectDirectories.Service
    const db = (yield* Database.Service).db
    const events = yield* EventV2.Service

    const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) {
      if (update) yield* events.publish(Event.Updated, { projectID })
    })

    const canonical = Effect.fnUntraced(function* (input: AbsolutePath) {
      const resolved = AbsolutePath.make(yield* fs.resolve(input))
      if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input })
      return resolved
    })

    const registry = new Map<StrategyID, Strategy>()

    const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) {
      if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id })
      registry.set(strategy.id, strategy)
    })

    // Register default strategies
    yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie)

    const strategies = () => Array.from(registry.values())

    const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) {
      const sourceDirectory = yield* canonical(input)
      if (!(yield* directories.contains({ projectID, directory: sourceDirectory })))
        return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory })
      return sourceDirectory
    })

    const getStrategy = Effect.fnUntraced(function* (id: StrategyID) {
      const found = registry.get(id)
      if (!found) return yield* new StrategyUnavailableError({ strategy: id })
      return found
    })

    const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) {
      const selected = yield* getStrategy(input.strategy)
      const sourceDirectory = yield* source(input.sourceDirectory, input.projectID)
      yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie)
      const name = input.name ?? Slug.create()
      let suffix = 1
      let copyDirectory = AbsolutePath.make(path.join(input.directory, name))
      while (yield* fs.existsSafe(copyDirectory)) {
        suffix++
        if (suffix > 10) return yield* new DestinationExistsError({ directory: copyDirectory })
        copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`))
      }

      const result = yield* selected.create({
        directory: copyDirectory,
        sourceDirectory,
      })
      yield* changed(
        input.projectID,
        yield* directories.create({
          projectID: input.projectID,
          directory: result.directory,
          strategy: input.strategy,
          behavior: "replace",
        }),
      )
      return result
    })

    const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) {
      const copyDirectory = yield* canonical(input.directory)
      const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory })
      if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory })
      yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({
        directory: copyDirectory,
        force: input.force,
      })
      yield* changed(
        input.projectID,
        yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }),
      )
    })

    const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) {
      const stored = yield* directories.list(input.projectID)
      const checked = yield* Effect.forEach(
        stored,
        (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))),
        { concurrency: "unbounded" },
      )
      const sourceDirectories = checked
        .filter((item) => item.strategy === undefined && item.exists)
        .map((item) => item.directory)
      const discovered = yield* Effect.forEach(
        sourceDirectories,
        (sourceDirectory) =>
          Effect.forEach(strategies(), (strategy) =>
            strategy.list(sourceDirectory).pipe(
              Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])),
              Effect.map((items) =>
                items.map((item) => ({
                  directory: item.directory,
                  strategy: item.type === "copy" ? strategy.id : undefined,
                })),
              ),
            ),
          ),
        { concurrency: "unbounded" },
      ).pipe(
        Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()),
      )
      const removed = checked.filter((item) => !item.exists).map((item) => item.directory)
      const result = yield* db
        .transaction((tx) =>
          Effect.all({
            updated: Effect.forEach(discovered, (item) =>
              directories.create(
                {
                  projectID: input.projectID,
                  directory: item.directory,
                  strategy: item.strategy,
                  behavior: "replace",
                },
                tx,
              ),
            ),
            removed: Effect.forEach(removed, (directory) =>
              directories.remove({ projectID: input.projectID, directory }, tx),
            ),
          }),
        )
        .pipe(Effect.orDie)
      const changes = {
        updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory),
        removed: removed.filter((_, index) => result.removed[index]),
      }
      yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0)
      return changes
    })

    return Service.of({
      register,
      create,
      remove,
      refresh,
    })
  }),
)

export const locationLayer = layer
export const node = makeLocationNode({
  service: Service,
  layer: layer,
  deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node],
})

export const refreshNode = makeLocationNode({
  name: "project-copy-refresh",
  layer: Layer.effectDiscard(refreshAfterBoot),
  deps: [node, Location.node],
})
