import fs from "node:fs"
import { normalizeComparablePath } from "@anybox/platform"
import path from "node:path"
import type { WorkspaceFileChangeIPCEvent } from "../shared/desktop-ipc-contract"
import { DESKTOP_WORKSPACE_FILE_CHANGE_EVENT_CHANNEL } from "../shared/desktop-ipc-contract"

export const WORKSPACE_FILE_CHANGE_EVENT_CHANNEL = DESKTOP_WORKSPACE_FILE_CHANGE_EVENT_CHANNEL

const WORKSPACE_WATCH_DEBOUNCE_MS = 300
const IGNORED_WORKSPACE_WATCH_DIRECTORIES = new Set([
  ".cache",
  ".next",
  ".turbo",
  ".vite",
  "build",
  "coverage",
  "dist",
  "node_modules",
  "out",
  "target",
])

export type { WorkspaceFileChangeIPCEvent }

type SenderLike = {
  id: number
  isDestroyed: () => boolean
  once: (event: "destroyed", listener: () => void) => unknown
  send: (channel: string, payload: WorkspaceFileChangeIPCEvent) => void
}
type WatchFactory = (filename: string, options: fs.WatchOptions, listener: fs.WatchListener<string>) => fs.FSWatcher
type ExistsSync = typeof fs.existsSync
type PathModule = typeof path.win32

interface WorkspaceWatchManagerOptions {
  platform?: NodeJS.Platform
}

interface DirectoryWatchState {
  directory: string
  gitDirectory: string
  sender: SenderLike
  rootWatcher: fs.FSWatcher | null
  gitWatcher: fs.FSWatcher | null
  pendingPaths: Set<string>
  debounceTimer: ReturnType<typeof setTimeout> | null
}

function getPathModule(platform: NodeJS.Platform): PathModule {
  return platform === "win32" ? path.win32 : path.posix
}

function normalizeDirectory(input: string, pathModule: PathModule, platform: NodeJS.Platform) {
  const resolved = pathModule.resolve(input.trim())
  const normalized = pathModule.normalize(resolved)
  return normalizeComparablePath(normalized, platform)
}

function resolveDirectory(input: string, pathModule: PathModule) {
  return pathModule.normalize(pathModule.resolve(input.trim()))
}

function isGitPath(directory: string, changedPath: string, pathModule: PathModule) {
  const relativePath = pathModule.relative(directory, changedPath)
  if (!relativePath) return false
  return relativePath === ".git" || relativePath.startsWith(`.git${pathModule.sep}`)
}

function isIgnoredWorkspacePath(directory: string, changedPath: string, pathModule: PathModule) {
  if (isGitPath(directory, changedPath, pathModule)) return false
  const relativePath = pathModule.relative(directory, changedPath)
  if (!relativePath || relativePath.startsWith("..") || pathModule.isAbsolute(relativePath)) return false
  return relativePath
    .split(/[\\/]+/)
    .some((segment) => IGNORED_WORKSPACE_WATCH_DIRECTORIES.has(segment))
}

function resolveChangedPath(baseDirectory: string, filename: string | Buffer | null | undefined, pathModule: PathModule) {
  if (typeof filename !== "string") return baseDirectory
  const trimmed = filename.trim()
  if (!trimmed) return baseDirectory
  return pathModule.normalize(pathModule.resolve(baseDirectory, trimmed))
}

function safeSend(sender: SenderLike, payload: WorkspaceFileChangeIPCEvent) {
  if (sender.isDestroyed()) return
  sender.send(WORKSPACE_FILE_CHANGE_EVENT_CHANNEL, payload)
}

function safeExists(existsSync: ExistsSync, targetDirectory: string) {
  try {
    return existsSync(targetDirectory)
  } catch {
    return false
  }
}

export class WorkspaceWatchManager {
  private readonly trackedSenders = new Set<number>()
  private readonly senderStates = new Map<number, Map<string, DirectoryWatchState>>()
  private readonly pathModule: PathModule
  private readonly platform: NodeJS.Platform

  constructor(
    private readonly watchFactory: WatchFactory = fs.watch as WatchFactory,
    private readonly existsSync: ExistsSync = fs.existsSync,
    options: WorkspaceWatchManagerOptions = {},
  ) {
    this.platform = options.platform ?? process.platform
    this.pathModule = getPathModule(this.platform)
  }

  updateDirectories(sender: SenderLike, directories: string[]) {
    this.ensureSenderCleanup(sender)

    const nextDirectories = new Map<string, string>()
    for (const directory of directories) {
      const trimmed = directory.trim()
      if (!trimmed) continue
      const resolvedDirectory = resolveDirectory(trimmed, this.pathModule)
      if (!safeExists(this.existsSync, resolvedDirectory)) continue
      nextDirectories.set(normalizeDirectory(resolvedDirectory, this.pathModule, this.platform), resolvedDirectory)
    }

    const currentStates = this.senderStates.get(sender.id) ?? new Map<string, DirectoryWatchState>()

    for (const [directoryKey, state] of currentStates.entries()) {
      if (nextDirectories.has(directoryKey)) continue
      this.disposeDirectoryState(state)
      currentStates.delete(directoryKey)
    }

    for (const [directoryKey, resolvedDirectory] of nextDirectories.entries()) {
      if (currentStates.has(directoryKey)) continue
      const nextState = this.createDirectoryState(sender, resolvedDirectory)
      if (!nextState) continue
      currentStates.set(directoryKey, nextState)
    }

    if (currentStates.size === 0) {
      this.senderStates.delete(sender.id)
    } else {
      this.senderStates.set(sender.id, currentStates)
    }

    return [...currentStates.values()].map((state) => state.directory)
  }

  dispose() {
    for (const senderID of [...this.senderStates.keys()]) {
      this.disposeSender(senderID)
    }
  }

  private ensureSenderCleanup(sender: SenderLike) {
    if (this.trackedSenders.has(sender.id)) return
    this.trackedSenders.add(sender.id)

    sender.once("destroyed", () => {
      this.trackedSenders.delete(sender.id)
      this.disposeSender(sender.id)
    })
  }

  private disposeSender(senderID: number) {
    const states = this.senderStates.get(senderID)
    if (!states) return

    for (const state of states.values()) {
      this.disposeDirectoryState(state)
    }

    this.senderStates.delete(senderID)
  }

  private createDirectoryState(sender: SenderLike, directory: string): DirectoryWatchState | null {
    const state: DirectoryWatchState = {
      directory,
      gitDirectory: this.pathModule.join(directory, ".git"),
      sender,
      rootWatcher: null,
      gitWatcher: null,
      pendingPaths: new Set<string>(),
      debounceTimer: null,
    }

    state.rootWatcher = this.createWatcher(directory, (_eventType, filename) => {
      this.recordChange(state, directory, filename)
    })
    if (!state.rootWatcher) return null

    this.syncGitWatcher(state)
    return state
  }

  private createWatcher(targetDirectory: string, listener: fs.WatchListener<string>) {
    try {
      return this.watchFactory(targetDirectory, { recursive: true }, listener)
    } catch {
      try {
        return this.watchFactory(targetDirectory, {}, listener)
      } catch {
        return null
      }
    }
  }

  private syncGitWatcher(state: DirectoryWatchState) {
    const gitDirectoryExists = this.existsSync(state.gitDirectory)
    if (!gitDirectoryExists) {
      if (state.gitWatcher) {
        state.gitWatcher.close()
        state.gitWatcher = null
      }
      return
    }

    if (state.gitWatcher) return

    state.gitWatcher = this.createWatcher(state.gitDirectory, (_eventType, filename) => {
      this.recordChange(state, state.gitDirectory, filename)
    })
  }

  private recordChange(state: DirectoryWatchState, baseDirectory: string, filename: string | Buffer | null | undefined) {
    const changedPath = resolveChangedPath(baseDirectory, filename, this.pathModule)
    if (baseDirectory === state.directory && isIgnoredWorkspacePath(state.directory, changedPath, this.pathModule)) {
      return
    }

    state.pendingPaths.add(changedPath)

    if (baseDirectory === state.directory ? isGitPath(state.directory, changedPath, this.pathModule) : true) {
      this.syncGitWatcher(state)
    }

    if (state.debounceTimer) {
      clearTimeout(state.debounceTimer)
    }

    state.debounceTimer = setTimeout(() => {
      state.debounceTimer = null
      const paths = [...state.pendingPaths]
      state.pendingPaths.clear()
      safeSend(state.sender, {
        directory: state.directory,
        paths: paths.length > 0 ? paths : [state.directory],
      })
    }, WORKSPACE_WATCH_DEBOUNCE_MS)
  }

  private disposeDirectoryState(state: DirectoryWatchState) {
    if (state.debounceTimer) {
      clearTimeout(state.debounceTimer)
      state.debounceTimer = null
    }

    state.rootWatcher?.close()
    state.rootWatcher = null
    state.gitWatcher?.close()
    state.gitWatcher = null
    state.pendingPaths.clear()
  }
}
