import { randomId } from '../randomId'
import type { DirEntry, ListDirResult } from '../types/ipc-types'
import {
  dirnamePath,
  fileUrlToPath,
  isAbsDiskPath,
  joinAbsPath,
  normalizeAbsPath,
  pathBasename,
  pathsEqual,
  resolvePathFromBaseFile
} from '../pathLite'

const MD_EXT = new Set(['.md', '.markdown', '.mdown', '.mkd'])
const IMAGE_EXT = new Set([
  '.png',
  '.jpg',
  '.jpeg',
  '.gif',
  '.webp',
  '.svg',
  '.bmp',
  '.ico',
  '.avif'
])

function isMarkdownFile(name: string): boolean {
  const lower = name.toLowerCase()
  const dot = lower.lastIndexOf('.')
  if (dot < 0) return false
  return MD_EXT.has(lower.slice(dot))
}

function isImageFile(name: string): boolean {
  const lower = name.toLowerCase()
  const dot = lower.lastIndexOf('.')
  if (dot < 0) return false
  return IMAGE_EXT.has(lower.slice(dot))
}

function parentPath(p: string): string {
  const n = normalizeAbsPath(p).replace(/\/+$/, '')
  const i = n.lastIndexOf('/')
  if (i <= 0) return '/'
  const d = n.slice(0, i)
  if (/^[A-Za-z]:$/.test(d)) return `${d}/`
  return d || '/'
}

function baseName(p: string): string {
  const n = p.replace(/\\/g, '/').replace(/\/+$/, '')
  const i = n.lastIndexOf('/')
  return i >= 0 ? n.slice(i + 1) : n
}

type DirEntries = FileSystemDirectoryHandle & {
  entries: () => AsyncIterableIterator<[string, FileSystemHandle]>
}

async function* iterateDir(
  dir: FileSystemDirectoryHandle
): AsyncGenerator<[string, FileSystemHandle]> {
  const d = dir as DirEntries
  for await (const entry of d.entries()) {
    yield entry
  }
}

/** 浏览器端：File System Access 句柄 + 本机绝对路径（用户确认）映射。 */
class WebVfs {
  private dirHandles = new Map<string, FileSystemDirectoryHandle>()
  private fileHandles = new Map<string, FileSystemFileHandle>()
  private memoryFiles = new Map<string, File>()
  private blobUrlByPath = new Map<string, string>()
  private defaultDir = ''

  getDefaultDirectory(): string {
    return this.defaultDir
  }

  setDefaultDirectory(p: string): void {
    this.defaultDir = normalizeAbsPath(p)
  }

  hasPath(path: string): boolean {
    const n = normalizeAbsPath(path)
    return this.fileHandles.has(n) || this.memoryFiles.has(n) || this.dirHandles.has(n)
  }

  /** 仅通过 <input type="file"> 打开、无 FileHandle，无法写回用户磁盘原文件。 */
  isMemoryOnlyPath(path: string): boolean {
    const n = normalizeAbsPath(path)
    if (this.fileHandles.has(n)) return false
    return this.memoryFiles.has(n)
  }

  getCachedBlobUrlPublic(absPath: string): string | undefined {
    return this.getCachedBlobUrl(absPath)
  }

  listKnownImagePaths(): string[] {
    return Array.from(this.blobUrlByPath.keys())
  }

  async registerDirectory(root: FileSystemDirectoryHandle, absRoot: string): Promise<string> {
    const rootPath = normalizeAbsPath(absRoot)
    this.dirHandles.set(rootPath, root)
    this.defaultDir = rootPath
    await this.indexDirectory(root, rootPath, true)
    return rootPath
  }

  private async indexDirectory(
    dir: FileSystemDirectoryHandle,
    absPath: string,
    deep: boolean
  ): Promise<void> {
    for await (const [name, handle] of iterateDir(dir)) {
      if (name.startsWith('.')) continue
      const full = joinAbsPath(absPath, name)
      if (handle.kind === 'directory') {
        const sub = handle as FileSystemDirectoryHandle
        this.dirHandles.set(full, sub)
        if (deep) await this.indexDirectory(sub, full, true)
      } else if (handle.kind === 'file') {
        const fh = handle as FileSystemFileHandle
        if (isMarkdownFile(name)) {
          this.fileHandles.set(full, fh)
        }
        if (isImageFile(name)) {
          void this.cacheBlobUrl(full, fh)
        }
      }
    }
  }

  private async cacheBlobUrl(absPath: string, handle: FileSystemFileHandle): Promise<string> {
    const key = normalizeAbsPath(absPath)
    const prev = this.blobUrlByPath.get(key)
    if (prev) return prev
    const file = await handle.getFile()
    const url = URL.createObjectURL(file)
    this.blobUrlByPath.set(key, url)
    return url
  }

  /** 将图片 src（含 file://）解析为可显示的 blob URL。 */
  async resolveImageToBlobUrl(src: string, baseFilePath: string | null): Promise<string | null> {
    const s = src.trim()
    if (!s) return null
    if (/^(blob:|data:|https?:)/i.test(s)) return s

    const base = baseFilePath?.trim() ? normalizeAbsPath(baseFilePath) : null
    const absPath = this.resolveImageAbsPath(s, base)
    if (!absPath) return null

    const hit = await this.ensureBlobUrl(absPath)
    if (hit) return hit

    if (base) {
      const baseDir = dirnamePath(base)
      const imgDir = dirnamePath(absPath)
      if (pathsEqual(baseDir, imgDir)) {
        const sibling = joinAbsPath(baseDir, pathBasename(absPath))
        if (!pathsEqual(sibling, absPath)) {
          const alt = await this.ensureBlobUrl(sibling)
          if (alt) return alt
        }
      }
    }

    return this.ensureBlobUrlByBasename(pathBasename(absPath))
  }

  resolveImageAbsPath(src: string, baseFilePath: string | null): string | null {
    const s = src.trim()
    if (/^file:/i.test(s)) return normalizeAbsPath(fileUrlToPath(s))
    if (isAbsDiskPath(s)) return normalizeAbsPath(s)
    if (baseFilePath) return normalizeAbsPath(resolvePathFromBaseFile(baseFilePath, s))
    return null
  }

  private lookupFileHandle(
    absPath: string
  ): { key: string; handle: FileSystemFileHandle } | null {
    const key = normalizeAbsPath(absPath)
    const direct = this.fileHandles.get(key)
    if (direct) return { key, handle: direct }

    for (const [k, h] of this.fileHandles) {
      if (pathsEqual(k, key)) return { key: k, handle: h }
    }
    return null
  }

  private lookupImageByBasename(fileName: string): { key: string; handle: FileSystemFileHandle } | null {
    const want = fileName.toLowerCase()
    let found: { key: string; handle: FileSystemFileHandle } | null = null
    for (const [k, h] of this.fileHandles) {
      if (!isImageFile(pathBasename(k))) continue
      if (pathBasename(k).toLowerCase() !== want) continue
      if (!found) found = { key: k, handle: h }
    }
    return found
  }

  private async ensureBlobUrl(absPath: string): Promise<string | null> {
    const key = normalizeAbsPath(absPath)
    const cached = this.getCachedBlobUrl(key)
    if (cached) return cached

    const located = this.lookupFileHandle(key)
    let handle = located?.handle
    let storeKey = located?.key ?? key

    if (!handle) {
      handle = (await this.ensureFileHandle(key)) ?? undefined
      storeKey = key
    }
    if (!handle) return null
    return this.cacheBlobUrl(storeKey, handle)
  }

  private async ensureBlobUrlByBasename(fileName: string): Promise<string | null> {
    const located = this.lookupImageByBasename(fileName)
    if (!located) return null
    return this.ensureBlobUrl(located.key)
  }

  private getCachedBlobUrl(absPath: string): string | undefined {
    const key = normalizeAbsPath(absPath)
    const direct = this.blobUrlByPath.get(key)
    if (direct) return direct
    for (const [k, url] of this.blobUrlByPath) {
      if (pathsEqual(k, key)) return url
    }
    return undefined
  }

  async registerOpenFile(handle: FileSystemFileHandle, absPath: string): Promise<string> {
    const normalized = normalizeAbsPath(absPath)
    this.fileHandles.set(normalized, handle)
    this.memoryFiles.delete(normalized)
    this.defaultDir = dirnamePath(normalized)
    return normalized
  }

  /** 为「仅打开文件」生成稳定虚拟路径（无需用户填写磁盘绝对路径）。 */
  virtualPathForPickedFile(fileName: string): string {
    const id = randomId()
    const safe =
      fileName.replace(/[/\\<>|:*?"\u0000-\u001f]/g, '_').trim() || 'note.md'
    return normalizeAbsPath(`/__web__/open/${id}/${safe}`)
  }

  registerMemoryFile(absPath: string, file: File): string {
    const normalized = normalizeAbsPath(absPath)
    this.memoryFiles.set(normalized, file)
    return normalized
  }

  async readFile(path: string): Promise<string> {
    const normalized = normalizeAbsPath(path)
    const mem = this.memoryFiles.get(normalized)
    if (mem) return mem.text()
    let h = this.fileHandles.get(normalized)
    if (!h) {
      h = (await this.ensureFileHandle(normalized)) ?? undefined
    }
    if (!h) throw new Error(`文件不存在或未授权：${normalized}`)
    const file = await h.getFile()
    return file.text()
  }

  async writeFile(path: string, content: string): Promise<void> {
    const normalized = normalizeAbsPath(path)
    const mem = this.memoryFiles.get(normalized)
    if (mem) {
      this.memoryFiles.set(normalized, new File([content], mem.name, { type: 'text/markdown' }))
      return
    }
    let h = this.fileHandles.get(normalized)
    if (!h) {
      h = await this.createFileAtParent(normalized)
    }
    const w = await h.createWritable()
    await w.write(content)
    await w.close()
  }

  private async createFileAtParent(path: string): Promise<FileSystemFileHandle> {
    const parent = parentPath(path)
    const name = baseName(path)
    const dir = this.dirHandles.get(parent)
    if (!dir) throw new Error(`无法写入：父目录未打开 (${parent})`)
    const h = await dir.getFileHandle(name, { create: true })
    this.fileHandles.set(path, h)
    return h
  }

  async listDirectory(dirPath: string): Promise<ListDirResult> {
    const normalized = normalizeAbsPath(dirPath)
    const dir = this.dirHandles.get(normalized)
    if (!dir) {
      return { entries: [], error: '未授权访问该目录（Web 版请使用工具栏「打开」选择文件）。' }
    }
    try {
      const entries: DirEntry[] = []
      for await (const [name, handle] of iterateDir(dir)) {
        if (name.startsWith('.')) continue
        const full = joinAbsPath(normalized, name)
        if (handle.kind === 'directory') {
          this.dirHandles.set(full, handle as FileSystemDirectoryHandle)
          entries.push({ name, path: full, kind: 'dir' })
        } else if (handle.kind === 'file' && isMarkdownFile(name)) {
          this.fileHandles.set(full, handle as FileSystemFileHandle)
          entries.push({ name, path: full, kind: 'file' })
        }
      }
      entries.sort((a, b) => {
        if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1
        return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
      })
      return { entries }
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e)
      return { entries: [], error: msg }
    }
  }

  parentDirectory(dirPath: string): string {
    const n = normalizeAbsPath(dirPath)
    if (!n) return n
    if (isAbsDiskPath(n)) {
      const p = parentPath(n)
      return p
    }
    return parentPath(n)
  }

  private findWorkspaceRoot(absPath: string): { rootPath: string; dir: FileSystemDirectoryHandle } | null {
    const normalized = normalizeAbsPath(absPath)
    let best = ''
    let bestDir: FileSystemDirectoryHandle | null = null
    for (const [rp, dir] of this.dirHandles) {
      const root = normalizeAbsPath(rp)
      if (normalized === root || normalized.startsWith(`${root}/`)) {
        if (root.length > best.length) {
          best = root
          bestDir = dir
        }
      }
    }
    return bestDir ? { rootPath: best, dir: bestDir } : null
  }

  async ensureFileHandle(absPath: string): Promise<FileSystemFileHandle | null> {
    const normalized = normalizeAbsPath(absPath)
    const direct = this.fileHandles.get(normalized)
    if (direct) return direct

    const ws = this.findWorkspaceRoot(normalized)
    if (!ws) return null

    const relParts = normalized.slice(ws.rootPath.length).split('/').filter(Boolean)
    if (relParts.length === 0) return null

    try {
      let cur = ws.dir
      let curPath = ws.rootPath
      for (let i = 0; i < relParts.length - 1; i++) {
        const part = relParts[i]!
        cur = await cur.getDirectoryHandle(part)
        curPath = joinAbsPath(curPath, part)
        this.dirHandles.set(curPath, cur)
      }
      const fileName = relParts[relParts.length - 1]!
      const fh = await cur.getFileHandle(fileName)
      this.fileHandles.set(normalized, fh)
      if (isImageFile(fileName)) void this.cacheBlobUrl(normalized, fh)
      return fh
    } catch {
      return null
    }
  }

  async deleteFile(path: string): Promise<void> {
    const normalized = normalizeAbsPath(path)
    const parent = parentPath(normalized)
    const name = baseName(normalized)
    const dir = this.dirHandles.get(parent)
    if (!dir) throw new Error('无法删除：父目录未找到')
    await dir.removeEntry(name)
    this.fileHandles.delete(normalized)
  }

  async renamePath(oldPath: string, newName: string): Promise<{ newPath: string }> {
    const oldNorm = normalizeAbsPath(oldPath)
    const parent = parentPath(oldNorm)
    const dir = this.dirHandles.get(parent)
    if (!dir) throw new Error('无法重命名：父目录未找到')
    const newPath = joinAbsPath(parent, newName.trim())
    const handle = this.fileHandles.get(oldNorm) ?? this.dirHandles.get(oldNorm)
    if (!handle) throw new Error('无法重命名：条目未找到')
    const mover = dir as FileSystemDirectoryHandle & {
      move?: (item: FileSystemHandle, name: string) => Promise<void>
    }
    if (typeof mover.move !== 'function') {
      throw new Error('当前浏览器不支持重命名，请在资源管理器中手动操作')
    }
    await mover.move(handle, newName.trim())
    if (this.fileHandles.has(oldNorm)) {
      this.fileHandles.delete(oldNorm)
      if (handle.kind === 'file') this.fileHandles.set(newPath, handle as FileSystemFileHandle)
    } else if (this.dirHandles.has(oldNorm)) {
      this.dirHandles.delete(oldNorm)
      if (handle.kind === 'directory') this.dirHandles.set(newPath, handle as FileSystemDirectoryHandle)
    }
    return { newPath }
  }

  async pickSaveHandle(
    suggestedName?: string
  ): Promise<FileSystemFileHandle | null> {
    if (typeof window.isSecureContext === 'boolean' && !window.isSecureContext) {
      return null
    }
    if ('showSaveFilePicker' in window) {
      try {
        return await window.showSaveFilePicker!({
          suggestedName: suggestedName || '未命名.md',
          types: [
            {
              description: 'Markdown',
              accept: { 'text/markdown': ['.md', '.markdown'] }
            }
          ]
        })
      } catch {
        return null
      }
    }
    return null
  }
}

export const webVfs = new WebVfs()

export function supportsFileSystemAccess(): boolean {
  if (typeof window.showOpenFilePicker !== 'function') return false
  // Chromium 上 API 可能「存在」但在非安全上下文调用会抛 SecurityError，必须走 <input type="file"> 降级。
  if (typeof window.isSecureContext === 'boolean' && !window.isSecureContext) return false
  return true
}

export function getCachedImageBlobUrl(absPath: string): string | undefined {
  return webVfs.getCachedBlobUrlPublic(normalizeAbsPath(absPath))
}
