// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import { fnmatch } from './fnmatch.ts'
import { rstripSlash, stripSlash } from './slash.ts'

export function norm(path: string): string {
  return `/${stripSlash(path)}`
}

// Whether a glob pattern matches `path` or one of its ancestors.
// Segment-wise fnmatch, so `*` does not cross `/` (shell glob semantics,
// not fnmatch's flat matching). An ancestor match covers entries living
// under a matched directory.
export function globPrefixMatch(path: string, pattern: string): boolean {
  const patSegs = stripSlash(pattern).split('/')
  const pathSegs = stripSlash(path).split('/')
  if (pathSegs.length < patSegs.length) return false
  return patSegs.every((pat, i) => fnmatch(pathSegs[i] ?? '', pat))
}

// Resolve a relative path against cwd; absolute paths are only
// normalized. Mirrors the Python utils resolve_path.
export function resolvePath(path: string, cwd: string): string {
  if (path.startsWith('/')) return posixNormpath(path)
  return posixNormpath(`${rstripSlash(cwd)}/${path}`)
}

// Mirror of Python's posixpath.normpath: resolves . and .. segments and
// collapses redundant slashes without touching the filesystem.
export function posixNormpath(path: string): string {
  if (path === '') return '.'
  const isAbs = path.startsWith('/')
  const parts = path.split('/').filter((p) => p !== '' && p !== '.')
  const stack: string[] = []
  for (const part of parts) {
    if (part === '..') {
      if (stack.length > 0 && stack[stack.length - 1] !== '..') {
        stack.pop()
      } else if (!isAbs) {
        stack.push('..')
      }
    } else {
      stack.push(part)
    }
  }
  const joined = stack.join('/')
  if (isAbs) return `/${joined}`
  return joined === '' ? '.' : joined
}

export function expandTilde(word: string, home: string | null): string {
  if (home === null) return word
  if (word === '~') return home
  if (word.startsWith('~/')) return rstripSlash(home) + word.slice(1)
  return word
}

// Rewrite the base of walked output paths (find/grep -r results) to the
// as-typed form (`PathSpec.rawPath`); `raw` equal to `virtual` leaves the
// paths unchanged (the absolute-argument case).
export function respellRaw(paths: string[], virtual: string, raw: string): string[] {
  if (raw === virtual) return paths
  return paths.map((p) => respellOne(p, virtual, raw))
}

// The prefix of `path` with `count` trailing segments removed. The ancestor
// counterpart of respellOne: it names a path above another one while keeping the
// original spelling, so a relative argument stays relative. `count` is clamped
// so the result never loses every segment, which would leave an empty string
// where a path belongs. Mirrors Python's drop_trailing_segments.
export function dropTrailingSegments(path: string, count: number): string {
  if (count <= 0) return path
  const parts = rstripSlash(path).split('/')
  if (count >= parts.filter((part) => part !== '').length) return path
  const joined = parts.slice(0, parts.length - count).join('/')
  return joined === '' ? '/' : joined
}

export function respellOne(path: string, virtual: string, raw: string): string {
  if (raw === virtual) return path
  const base = rstripSlash(virtual)
  if (path === base || (base === '' && path === '/')) return raw === '' ? '.' : raw
  if (path.startsWith(base + '/')) {
    // The empty raw is the synthetic no-operand spelling (GNU grep -r
    // with no path): results render as bare names relative to the base.
    if (raw === '') return path.slice(base.length + 1)
    return rstripSlash(raw) + path.slice(base.length)
  }
  return path
}

export function parent(path: string): string {
  const i = path.lastIndexOf('/')
  if (i <= 0) return '/'
  return path.slice(0, i)
}

// The proper ancestors of a normalized key, outermost first: "/a/b/c" ->
// ["/a", "/a/b"]; "/a" and "/" -> []. "/" is left out because every store
// treats the mount root as an existing directory, so it is never a component
// worth probing. Used by the store-backed backends (ram, redis) to walk a
// destination's parent chain the way rename(2) resolves it.
export function ancestors(path: string): string[] {
  const parts = stripSlash(path).split('/')
  const out: string[] = []
  for (let i = 1; i < parts.length; i++) out.push(`/${parts.slice(0, i).join('/')}`)
  return out
}

export const MAX_SYMLINK_HOPS = 40

// Raised when symlink resolution exceeds the maximum hop count. Mirrors POSIX
// ELOOP (a loop such as `a -> b -> a` or an unbounded expansion such as
// `a -> a/x`). Command boundaries render this as the GNU strerror text
// "Too many levels of symbolic links".
export class CycleError extends Error {
  readonly path: string
  // The condition's name in the shared vocabulary (errors/classify.ts
  // keys on codes), so a loop stops degrading to EIO at the kernel and
  // guest boundaries.
  readonly code = 'ELOOP'

  constructor(path: string) {
    super(`too many levels of symbolic links: ${path}`)
    this.name = 'CycleError'
    this.path = path
  }
}

function isLinkPrefix(key: string, path: string): boolean {
  return path === key || path.startsWith(key + '/')
}

// Follow the symlink table over a whole-path lookup: repeatedly substitute the
// longest link prefix that matches `path` until no link applies, resolving
// relative targets lazily against the link's own parent. Throws CycleError
// once the hop count is exceeded (POSIX ELOOP).
export function resolveSymlinks(path: string, links: Map<string, string>): string {
  if (links.size === 0) return path
  for (let hop = 0; hop < MAX_SYMLINK_HOPS; hop++) {
    let best: string | null = null
    let bestTarget = ''
    for (const [key, value] of links) {
      if (isLinkPrefix(key, path) && (best === null || key.length > best.length)) {
        best = key
        bestTarget = value
      }
    }
    if (best === null) return path
    let target = bestTarget
    if (!target.startsWith('/')) {
      target = norm(parent(best) + '/' + target)
    }
    path = target + path.slice(best.length)
  }
  throw new CycleError(path)
}

export function gnuBasename(path: string, suffix?: string): string {
  let i = path.length
  while (i > 0 && path[i - 1] === '/') i--
  if (i === 0) return path.length > 0 ? '/' : ''
  const j = path.lastIndexOf('/', i - 1)
  let base = path.slice(j + 1, i)
  if (suffix !== undefined && suffix !== '' && base !== suffix && base.endsWith(suffix)) {
    base = base.slice(0, base.length - suffix.length)
  }
  return base
}

export function gnuDirname(path: string): string {
  if (path === '') return '.'
  let i = path.length
  while (i > 0 && path[i - 1] === '/') i--
  if (i === 0) return '/'
  let j = path.lastIndexOf('/', i - 1)
  if (j === -1) return '.'
  while (j > 0 && path[j - 1] === '/') j--
  if (j === 0) return '/'
  return path.slice(0, j)
}
