import { InstanceState } from "@/effect/instance-state"
import { Git } from "@/git"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { Effect, Schema } from "effect"
import path from "path"
import * as Log from "@saeeol/core/util/log"
import { NonNegativeInt, type DeepMutable, withStatics } from "@/util/schema"
import { zod } from "@/util/effect-zod"

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

export const Info = Schema.Struct({
  path: Schema.String,
  added: NonNegativeInt,
  removed: NonNegativeInt,
  status: Schema.Literals(["added", "deleted", "modified"]),
})
  .annotate({ identifier: "File" })
  .pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>

export type Entry = { files: string[]; dirs: string[] }

const binary = new Set([
  "exe", "dll", "pdb", "bin", "so", "dylib", "o", "a", "lib",
  "wav", "mp3", "ogg", "oga", "ogv", "ogx", "flac", "aac", "wma", "m4a", "weba",
  "mp4", "avi", "mov", "wmv", "flv", "webm", "mkv",
  "zip", "tar", "gz", "gzip", "bz", "bz2", "bzip", "bzip2", "7z", "rar", "xz", "lz", "z",
  "pdf", "doc", "docx", "ppt", "pptx", "xls", "xlsx",
  "dmg", "iso", "img", "vmdk",
  "ttf", "otf", "woff", "woff2", "eot",
  "sqlite", "db", "mdb",
  "apk", "ipa", "aab", "xapk", "app", "pkg", "deb", "rpm", "snap", "flatpak", "appimage",
  "msi", "msp",
  "jar", "war", "ear", "class", "kotlin_module", "dex", "vdex", "odex", "oat", "art",
  "wasm", "wat", "bc", "ll", "s",
  "ko", "sys", "drv", "efi", "rom", "com",
])

const image = new Set([
  "png", "jpg", "jpeg", "gif", "bmp", "webp", "ico", "tif", "tiff", "svg", "svgz",
  "avif", "apng", "jxl", "heic", "heif", "raw", "cr2", "nef", "arw", "dng", "orf", "raf", "pef", "x3f",
])

const text = new Set([
  "ts", "tsx", "mts", "cts", "mtsx", "ctsx", "js", "jsx", "mjs", "cjs",
  "sh", "bash", "zsh", "fish", "ps1", "psm1", "cmd", "bat",
  "json", "jsonc", "json5", "yaml", "yml", "toml", "md", "mdx", "txt", "xml",
  "html", "htm", "css", "scss", "sass", "less", "graphql", "gql", "sql",
  "ini", "cfg", "conf", "env",
])

const textName = new Set([
  "dockerfile", "makefile", ".gitignore", ".gitattributes",
  ".editorconfig", ".npmrc", ".nvmrc", ".prettierrc", ".eslintrc",
])

const mime: Record<string, string> = {
  png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
  bmp: "image/bmp", webp: "image/webp", ico: "image/x-icon", tif: "image/tiff",
  tiff: "image/tiff", svg: "image/svg+xml", svgz: "image/svg+xml", avif: "image/avif",
  apng: "image/apng", jxl: "image/jxl", heic: "image/heic", heif: "image/heif",
}

const ext = (file: string) => path.extname(file).toLowerCase().slice(1)
const name = (file: string) => path.basename(file).toLowerCase()
export const isImageByExtension = (file: string) => image.has(ext(file))
export const isTextByExtension = (file: string) => text.has(ext(file))
export const isTextByName = (file: string) => textName.has(name(file))
export const isBinaryByExtension = (file: string) => binary.has(ext(file))
export const isImage = (mimeType: string) => mimeType.startsWith("image/")
export const getImageMimeType = (file: string) => mime[ext(file)] || "image/" + ext(file)

export function shouldEncode(mimeType: string) {
  const type = mimeType.toLowerCase()
  log.debug("shouldEncode", { type })
  if (!type) return false
  if (type.startsWith("text/")) return false
  if (type.includes("charset=")) return false
  const top = type.split("/", 2)[0]
  return ["image", "audio", "video", "font", "model", "multipart"].includes(top)
}

export function makeStatusOps(git: any, appFs: any) {
  const gitText = Effect.fnUntraced(function* (args: string[]) {
    return (yield* git.run(args, { cwd: (yield* InstanceState.context).directory })).text()
  })

  const status = Effect.fn("File.status")(function* () {
    const ctx = yield* InstanceState.context
    if (ctx.project.vcs !== "git") return []

    const diffOutput = yield* gitText([
      "-c",
      "core.fsmonitor=false",
      "-c",
      "core.quotepath=false",
      "diff",
      "--numstat",
      "HEAD",
    ])

    const changed: Info[] = []

    if (diffOutput.trim()) {
      for (const line of diffOutput.trim().split("\n")) {
        const [added, removed, file] = line.split("\t")
        changed.push({
          path: file,
          added: added === "-" ? 0 : parseInt(added, 10),
          removed: removed === "-" ? 0 : parseInt(removed, 10),
          status: "modified",
        })
      }
    }

    const untrackedOutput = yield* gitText([
      "-c",
      "core.fsmonitor=false",
      "-c",
      "core.quotepath=false",
      "ls-files",
      "--others",
      "--exclude-standard",
    ])

    if (untrackedOutput.trim()) {
      for (const file of untrackedOutput.trim().split("\n")) {
        const content = yield* appFs
          .readFileString(path.join(ctx.directory, file))
          .pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
        if (content === undefined) continue
        changed.push({
          path: file,
          added: content.split("\n").length,
          removed: 0,
          status: "added",
        })
      }
    }

    const deletedOutput = yield* gitText([
      "-c",
      "core.fsmonitor=false",
      "-c",
      "core.quotepath=false",
      "diff",
      "--name-only",
      "--diff-filter=D",
      "HEAD",
    ])

    if (deletedOutput.trim()) {
      for (const file of deletedOutput.trim().split("\n")) {
        changed.push({
          path: file,
          added: 0,
          removed: 0,
          status: "deleted",
        })
      }
    }

    return changed.map((item) => {
      const full = path.isAbsolute(item.path) ? item.path : path.join(ctx.directory, item.path)
      return {
        ...item,
        path: path.relative(ctx.directory, full),
      }
    })
  })

  return { gitText, status }
}
