/**
 * SANDCODE.md memory file loader.
 *
 * Walks the cwd → root chain and ~/.sandcode for SANDCODE.md (and
 * SANDCODE.local.md) files. Each file is loaded into a {@link MemoryFileInfo}
 * shape and concatenated into the system prompt by getSandcodeMds(). This
 * is the per-project + per-user instruction-loading core feature that the
 * agent reads at the start of every conversation.
 *
 * Discovery order (mirrors what the upstream module did before it was
 * deleted in an earlier refactor pass):
 *   1. ~/.sandcode/SANDCODE.md           (user scope)
 *   2. <cwd>/SANDCODE.md                 (project scope)
 *   3. <cwd>/SANDCODE.local.md           (project-local scope)
 *   4. <parent dirs>/SANDCODE.md         (walk up to repo root, then home)
 *   5. <additional --add-dir>/SANDCODE.md
 *
 * Caching: getMemoryFiles() memoizes its result for the process lifetime.
 * /clear and post-compact cleanup call clearMemoryFileCaches() to bust it.
 *
 * Conditional-rules helpers (getManagedAndUserConditionalRules,
 * getMemoryFilesForNestedDirectory, getConditionalRulesForCwdLevelDirectory)
 * return empty arrays for now — these implement the upstream's nested-dir
 * `<rules>`-block-conditional-on-tool-use feature, which sandcode hasn't
 * decided to support yet. Stubbing them keeps the attachments.ts call sites
 * working without surfacing partial rules.
 */

import { readFile, stat } from 'fs/promises'
import { homedir } from 'os'
import { dirname, join } from 'path'
import { getCwd } from './cwd.js'
import {
  getAdditionalDirectoriesForSandcodeMd,
  setCachedSandcodeMdContent,
} from '../bootstrap/state.js'
import { getSandcodeConfigHomeDir } from './envUtils.js'
import { logForDebugging } from './debug.js'

/** Above this character count a single SANDCODE.md is flagged as too large. */
export const MAX_MEMORY_CHARACTER_COUNT = 100_000

export type MemoryFileScope = 'project' | 'user' | 'enterprise' | 'parent' | 'nested'

export type MemoryFileInfo = {
  /** Absolute path to the SANDCODE.md file. */
  path: string
  /** Cached file contents (UTF-8). */
  content: string
  /** Where this file came from in the discovery walk. */
  scope: MemoryFileScope
  /** Type label used by attachments.ts conditional filters. */
  type: 'Project' | 'Local' | 'User' | 'Parent' | 'Enterprise'
  /** Character count — used by getLargeMemoryFiles. */
  characterCount: number
  /** True for files injected directly into the system prompt vs referenced. */
  isInjected?: boolean
}

const MEMORY_FILE_NAMES = ['SANDCODE.md', 'SANDCODE.local.md'] as const

let cachedMemoryFiles: MemoryFileInfo[] | null = null

async function readIfExists(path: string): Promise<string | null> {
  try {
    const s = await stat(path)
    if (!s.isFile()) return null
    return await readFile(path, 'utf-8')
  } catch {
    return null
  }
}

function* walkUp(start: string): Generator<string> {
  let current = start
  let last = ''
  while (current && current !== last) {
    yield current
    last = current
    current = dirname(current)
  }
}

async function loadFrom(
  dir: string,
  scope: MemoryFileScope,
  type: MemoryFileInfo['type'],
  collected: MemoryFileInfo[],
  seen: Set<string>,
): Promise<void> {
  for (const name of MEMORY_FILE_NAMES) {
    const path = join(dir, name)
    if (seen.has(path)) continue
    const content = await readIfExists(path)
    if (content === null) continue
    seen.add(path)
    collected.push({
      path,
      content,
      scope,
      type: name.endsWith('.local.md') ? 'Local' : type,
      characterCount: content.length,
      isInjected: true,
    })
  }
}

/**
 * Load SANDCODE.md files from user scope, project, parents, and additional dirs.
 * Memoized for the process lifetime; bust with clearMemoryFileCaches().
 *
 * @param forceRefresh skip the cache and re-read from disk
 */
export async function getMemoryFiles(forceRefresh = false): Promise<MemoryFileInfo[]> {
  if (!forceRefresh && cachedMemoryFiles !== null) {
    return cachedMemoryFiles
  }

  const collected: MemoryFileInfo[] = []
  const seen = new Set<string>()

  // 1. User scope: ~/.sandcode/SANDCODE.md
  try {
    await loadFrom(getSandcodeConfigHomeDir(), 'user', 'User', collected, seen)
  } catch (e) {
    logForDebugging(`[sandcodemd] user scope load failed: ${(e as Error).message}`)
  }

  // 2 + 3. Walk cwd → home root for project + parent SANDCODE.md files
  const cwd = getCwd()
  const home = homedir()
  let isFirst = true
  for (const dir of walkUp(cwd)) {
    // Stop walking once we cross above the home directory
    if (dir.length < home.length && !home.startsWith(dir)) break
    await loadFrom(
      dir,
      isFirst ? 'project' : 'parent',
      isFirst ? 'Project' : 'Parent',
      collected,
      seen,
    )
    isFirst = false
  }

  // 4. Additional --add-dir directories
  for (const extraDir of getAdditionalDirectoriesForSandcodeMd()) {
    await loadFrom(extraDir, 'nested', 'Project', collected, seen)
  }

  cachedMemoryFiles = collected

  // Mirror the legacy state slot so consumers reading the cached content
  // (compact's getCachedSandcodeMdContent) get a non-null value.
  if (collected.length > 0) {
    setCachedSandcodeMdContent(getSandcodeMds(collected))
  }

  return collected
}

/** Clear all in-memory caches. */
export function clearMemoryFileCaches(): void {
  cachedMemoryFiles = null
  setCachedSandcodeMdContent(null)
}

/** Reset just the getMemoryFiles cache (called from /clear). */
export function resetGetMemoryFilesCache(): void {
  cachedMemoryFiles = null
}

/**
 * Filter out memory files that have been injected into the system prompt
 * (so they aren't duplicated as additional context).
 */
export function filterInjectedMemoryFiles(files: MemoryFileInfo[]): MemoryFileInfo[] {
  return files.filter((f) => !f.isInjected)
}

/**
 * Concatenate memory files into a single string for the system prompt.
 * Each file is wrapped in a header comment so the model sees the source path.
 */
export function getSandcodeMds(files: MemoryFileInfo[]): string {
  if (files.length === 0) return ''
  return files.map((f) => `<!-- ${f.path} (${f.scope}) -->\n${f.content}`).join('\n\n')
}

/** Files exceeding MAX_MEMORY_CHARACTER_COUNT. */
export function getLargeMemoryFiles(files?: MemoryFileInfo[]): MemoryFileInfo[] {
  return (files ?? []).filter((f) => f.characterCount > MAX_MEMORY_CHARACTER_COUNT)
}

/**
 * Path-shape predicate — does this look like a SANDCODE.md file the
 * loader would have picked up? Used by the compaction layer to know
 * whether a tracked file edit should bust the memory cache.
 */
export function isMemoryFilePath(path: string): boolean {
  return /(?:^|[/\\])SANDCODE(?:\.local)?\.md$/i.test(path)
}

/**
 * External @-includes parsed out of all loaded SANDCODE.md files.
 * Sandcode doesn't yet implement `@./other-file.md`-style includes.
 */
export function getExternalSandcodeMdIncludes(_files: MemoryFileInfo[]): string[] {
  return []
}

/** Predicate version of {@link getExternalSandcodeMdIncludes}. */
export function hasExternalSandcodeMdIncludes(files: MemoryFileInfo[]): boolean {
  return getExternalSandcodeMdIncludes(files).length > 0
}

/**
 * Should the security-warning dialog render before the loader follows
 * external @-includes for the first time? Always false — no @-includes
 * are followed in this implementation.
 */
export async function shouldShowSandcodeMdExternalIncludesWarning(): Promise<boolean> {
  return false
}

// ─── Conditional-rules surface (currently no-ops) ─────────────────────────
// These implement the upstream's nested-dir-conditional `<rules>` block
// system. Sandcode hasn't decided to support that surface yet; the call
// sites in attachments.ts get empty arrays.

export async function getManagedAndUserConditionalRules(
  _filePath: string,
  _processedPaths: Set<string>,
): Promise<MemoryFileInfo[]> {
  return []
}

export async function getMemoryFilesForNestedDirectory(
  _nestedDir: string,
  _filePath: string,
  _processedPaths: Set<string>,
): Promise<MemoryFileInfo[]> {
  return []
}

export async function getConditionalRulesForCwdLevelDirectory(
  _cwdLevelDir: string,
  _filePath: string,
  _processedPaths: Set<string>,
): Promise<MemoryFileInfo[]> {
  return []
}
