import { readdir, readFile, writeFile, mkdir, rm, rename, stat, copyFile } from 'fs/promises';
import { resolve as pathResolve, join, normalize, dirname, extname } from 'path';
import crypto from 'crypto';
import { env } from '../../config/env';
import { redis } from '../../config/redis';
import { logger } from '../../utils/logger';
import { cm_docs_cache_hits, cm_docs_cache_misses } from '../../utils/metrics';

export interface FileNode {
  name: string;
  path: string;
  isDirectory: boolean;
  children?: FileNode[];
}

const DOCS_ROOT = pathResolve(env.MKDOCS_DOCS_PATH);

// Redis cache configuration
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
const TREE_CACHE_TTL = 30; // 30 seconds — short so external changes show quickly
const FILE_CONTENT_CACHE_TTL = 60 * 60; // 1 hour for file content

function hashFilePath(path: string): string {
  return crypto.createHash('sha256').update(path).digest('hex').substring(0, 16);
}

/**
 * Resolve and validate a relative path within MKDOCS_DOCS_PATH.
 * Throws if the resolved path escapes the docs root.
 */
function safeResolve(relativePath: string): string {
  const normalized = normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
  const resolved = pathResolve(DOCS_ROOT, normalized);
  // Use DOCS_ROOT + sep to prevent prefix attacks (e.g., /mkdocs/docs-evil matching /mkdocs/docs)
  if (resolved !== DOCS_ROOT && !resolved.startsWith(DOCS_ROOT + '/')) {
    throw new PathTraversalError();
  }
  return resolved;
}

export class PathTraversalError extends Error {
  constructor() {
    super('Path traversal not allowed');
    this.name = 'PathTraversalError';
  }
}

export class FileNotFoundError extends Error {
  constructor(filePath: string) {
    super(`File not found: ${filePath}`);
    this.name = 'FileNotFoundError';
  }
}

/**
 * Recursively list the file tree under MKDOCS_DOCS_PATH.
 * Cached in Redis with 1-hour TTL for root calls.
 */
async function listTree(dir: string = DOCS_ROOT, relBase: string = ''): Promise<FileNode[]> {
  // Try cache for root call only
  if (dir === DOCS_ROOT && !relBase) {
    try {
      const cached = await redis.get(TREE_CACHE_KEY);
      if (cached) {
        cm_docs_cache_hits.inc({ type: 'tree' });
        return JSON.parse(cached) as FileNode[];
      }
      cm_docs_cache_misses.inc({ type: 'tree' });
    } catch (err) {
      logger.warn('Failed to get cached docs tree:', err);
      cm_docs_cache_misses.inc({ type: 'tree' });
    }
  }

  const entries = await readdir(dir, { withFileTypes: true });

  const sorted = entries
    .filter(e => !e.name.startsWith('.'))
    .sort((a, b) => {
      if (a.isDirectory() && !b.isDirectory()) return -1;
      if (!a.isDirectory() && b.isDirectory()) return 1;
      return a.name.localeCompare(b.name);
    });

  // Parallel I/O instead of sequential for better performance
  const nodes = await Promise.all(
    sorted.map(async (entry) => {
      const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
      if (entry.isDirectory()) {
        const children = await listTree(join(dir, entry.name), relPath);
        return { name: entry.name, path: relPath, isDirectory: true, children };
      } else {
        return { name: entry.name, path: relPath, isDirectory: false };
      }
    })
  );

  // Cache root result
  if (dir === DOCS_ROOT && !relBase) {
    try {
      await redis.setex(TREE_CACHE_KEY, TREE_CACHE_TTL, JSON.stringify(nodes));
    } catch (err) {
      logger.warn('Failed to cache docs tree:', err);
    }
  }

  return nodes;
}

async function readFileContent(relativePath: string): Promise<string> {
  const cacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;

  // Try cache first
  try {
    const cached = await redis.get(cacheKey);
    if (cached) {
      cm_docs_cache_hits.inc({ type: 'file' });
      return cached;
    }
    cm_docs_cache_misses.inc({ type: 'file' });
  } catch (err) {
    logger.warn('Failed to get cached file content:', err);
    cm_docs_cache_misses.inc({ type: 'file' });
  }

  // Read from disk
  const fullPath = safeResolve(relativePath);
  try {
    const content = await readFile(fullPath, 'utf-8');

    // Cache the result
    try {
      await redis.setex(cacheKey, FILE_CONTENT_CACHE_TTL, content);
    } catch (err) {
      logger.warn('Failed to cache file content:', err);
    }

    return content;
  } catch (err: unknown) {
    if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
      throw new FileNotFoundError(relativePath);
    }
    throw err;
  }
}

async function writeFileContent(relativePath: string, content: string): Promise<void> {
  const fullPath = safeResolve(relativePath);
  await mkdir(dirname(fullPath), { recursive: true });
  await writeFile(fullPath, content, 'utf-8');

  // Invalidate file cache (content changed, structure unchanged)
  const cacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;
  try {
    await redis.del(cacheKey);
  } catch (err) {
    logger.warn('Failed to invalidate file cache:', err);
  }
}

async function createFile(relativePath: string, content?: string, isDirectory?: boolean): Promise<void> {
  const fullPath = safeResolve(relativePath);

  try {
    await stat(fullPath);
    throw new Error(`Already exists: ${relativePath}`);
  } catch (err: unknown) {
    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
      if ((err as Error).message?.startsWith('Already exists')) throw err;
      throw err;
    }
  }

  if (isDirectory) {
    await mkdir(fullPath, { recursive: true });
  } else {
    await mkdir(dirname(fullPath), { recursive: true });
    await writeFile(fullPath, content || '', 'utf-8');
  }

  // Invalidate tree cache (structure changed)
  try {
    await redis.del(TREE_CACHE_KEY);
  } catch (err) {
    logger.warn('Failed to invalidate tree cache:', err);
  }
}

async function deleteFile(relativePath: string): Promise<void> {
  const fullPath = safeResolve(relativePath);
  try {
    const info = await stat(fullPath);
    if (info.isDirectory()) {
      const entries = await readdir(fullPath);
      if (entries.length > 0) {
        throw new Error('Directory is not empty');
      }
      await rm(fullPath, { recursive: false });
    } else {
      await rm(fullPath);
    }
  } catch (err: unknown) {
    if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
      throw new FileNotFoundError(relativePath);
    }
    throw err;
  }

  // Invalidate both tree and file cache
  const fileCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(relativePath)}`;
  try {
    await Promise.all([
      redis.del(TREE_CACHE_KEY),
      redis.del(fileCacheKey),
    ]);
  } catch (err) {
    logger.warn('Failed to invalidate caches on delete:', err);
  }
}

async function renameFile(fromPath: string, toPath: string): Promise<void> {
  const fullFrom = safeResolve(fromPath);
  const fullTo = safeResolve(toPath);

  try {
    await stat(fullFrom);
  } catch {
    throw new FileNotFoundError(fromPath);
  }

  await mkdir(dirname(fullTo), { recursive: true });
  await rename(fullFrom, fullTo);

  // Invalidate tree and both file paths
  const fromCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(fromPath)}`;
  const toCacheKey = `${CACHE_KEY_PREFIX}file:${hashFilePath(toPath)}`;
  try {
    await Promise.all([
      redis.del(TREE_CACHE_KEY),
      redis.del(fromCacheKey),
      redis.del(toCacheKey),
    ]);
  } catch (err) {
    logger.warn('Failed to invalidate caches on rename:', err);
  }
}

function isEditableFile(relativePath: string): boolean {
  const ext = extname(relativePath).toLowerCase();
  return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
}

const ALLOWED_UPLOAD_EXTENSIONS = new Set([
  '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
  '.pdf', '.zip',
]);

async function uploadFile(relativePath: string, sourcePath: string): Promise<void> {
  const ext = extname(relativePath).toLowerCase();
  if (!ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
    throw new Error(`File type not allowed: ${ext}`);
  }

  const fullPath = safeResolve(relativePath);
  await mkdir(dirname(fullPath), { recursive: true });
  await copyFile(sourcePath, fullPath);

  // Invalidate tree cache (structure changed)
  try {
    await redis.del(TREE_CACHE_KEY);
  } catch (err) {
    logger.warn('Failed to invalidate tree cache after upload:', err);
  }
}

async function invalidateTreeCache(): Promise<void> {
  try {
    await redis.del(TREE_CACHE_KEY);
  } catch (err) {
    logger.warn('Failed to invalidate tree cache:', err);
  }
}

/**
 * Flatten a FileNode tree into file-only entries, then filter by
 * case-insensitive query match on name or path.  Name matches score
 * higher so they sort first.
 */
async function searchFiles(
  query: string,
  limit = 5,
): Promise<{ name: string; path: string }[]> {
  const tree = await listTree();
  const q = query.toLowerCase();

  const matches: { name: string; path: string; score: number }[] = [];

  function walk(nodes: FileNode[]) {
    for (const node of nodes) {
      if (node.isDirectory) {
        if (node.children) walk(node.children);
      } else {
        const nameLower = node.name.toLowerCase();
        const pathLower = node.path.toLowerCase();
        if (nameLower.includes(q)) {
          matches.push({ name: node.name, path: node.path, score: 2 });
        } else if (pathLower.includes(q)) {
          matches.push({ name: node.name, path: node.path, score: 1 });
        }
      }
    }
  }

  walk(tree);
  matches.sort((a, b) => b.score - a.score);
  return matches.slice(0, limit).map(({ name, path }) => ({ name, path }));
}

/**
 * Search within file contents for a query string.
 * Returns matching files with line numbers and context.
 */
async function searchContent(
  query: string,
  limit = 10,
): Promise<{ path: string; name: string; matches: { line: number; text: string; context: string }[] }[]> {
  if (!query || query.length < 2) return [];

  const tree = await listTree();
  const q = query.toLowerCase();
  const results: { path: string; name: string; matches: { line: number; text: string; context: string }[] }[] = [];

  async function walk(nodes: FileNode[]) {
    for (const node of nodes) {
      if (results.length >= limit) return;
      if (node.isDirectory) {
        if (node.children) await walk(node.children);
      } else if (node.name.endsWith('.md') || node.name.endsWith('.txt') || node.name.endsWith('.yml') || node.name.endsWith('.yaml')) {
        try {
          const content = await readFileContent(node.path);
          const lines = content.split('\n');
          const matches: { line: number; text: string; context: string }[] = [];

          for (let i = 0; i < lines.length && matches.length < 5; i++) {
            if (lines[i].toLowerCase().includes(q)) {
              const contextStart = Math.max(0, i - 1);
              const contextEnd = Math.min(lines.length - 1, i + 1);
              const context = lines.slice(contextStart, contextEnd + 1).join('\n');
              matches.push({ line: i + 1, text: lines[i].trim(), context });
            }
          }

          if (matches.length > 0) {
            results.push({ path: node.path, name: node.name, matches });
          }
        } catch {
          // Skip files that can't be read
        }
      }
    }
  }

  await walk(tree);
  return results;
}

export const docsFilesService = {
  listTree,
  readFileContent,
  writeFileContent,
  createFile,
  deleteFile,
  renameFile,
  uploadFile,
  safeResolve,
  isEditableFile,
  invalidateTreeCache,
  searchFiles,
  searchContent,
};
