<?php
namespace boru\boruai\Tools;

use boru\boruai\Models\Tool;
use boru\output\Output;
use boru\boruai\Models\ToolDefinition;
use boru\dhutils\ReflectionNamespace;
use Exception;
use RuntimeException;

/**
 * This class provides a set of utility functions to interact with the project's filesystem,
 * retrieve file contents, list directories, search for code patterns, and save or modify files.
 * 
 * It can be utilized as a "tool callback" layer that supplies a ChatGPT-based application with
 * the necessary contextual information about the project's codebase. ChatGPT could then use
 * these methods to read existing code, propose new implementations, and modify files accordingly.
 * 
 * Assumptions:
 * - The root directory of the project is statically defined.
 * - Classes are autoloaded with Composer or are available in the global scope.
 * - Methods provided here focus on generic filesystem operations and do not implement any
 *   actual ChatGPT calls. Instead, they serve as building blocks for a ChatGPT integration.
 */
class CodeManager {
    /**
     * @var string The absolute path to the project's root directory.
     */
    protected $rootDir;

    /**
     * Constructor.
     * 
     * @param string $rootDir The absolute path to the project's root directory.
     * @throws RuntimeException if the provided $rootDir is not a valid directory.
     */
    public function __construct(string $rootDir)
    {
        if (!is_dir($rootDir)) {
            throw new RuntimeException("Invalid root directory: $rootDir");
        }
        $this->rootDir = rtrim($rootDir, DIRECTORY_SEPARATOR);
    }

    /**
     * Get the absolute project root directory.
     * 
     * @return string The project root directory path.
     */
    public function getRootDirectory() {
        return $this->rootDir;
    }

    /**
     * List all files within a given directory, optionally recursively.
     * 
     * @param string $path      Path relative to the project root.
     * @param bool   $recursive If true, the listing will be recursive.
     * @return array List of file paths relative to the project root.
     * @throws RuntimeException if the directory does not exist.
     */
    public function listFiles(string $path = '', bool $recursive = false) {
        $dirPath = $this->getAbsolutePath($path);
        if (!is_dir($dirPath)) {
            throw new RuntimeException("Directory does not exist: $dirPath");
        }

        $files = [];

        $iterator = $recursive 
            ? new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dirPath, \FilesystemIterator::SKIP_DOTS))
            : new \DirectoryIterator($dirPath);

        foreach ($iterator as $item) {
            if ($item->isFile()) {
                $files[] = $this->getRelativePath((string) $item->getPathname());
            }
        }

        return $files;
    }

    /**
     * List all directories within a given directory, optionally recursively.
     *
     * @param string $path      Path relative to the project root.
     * @param bool   $recursive If true, the listing will be recursive.
     * @return array List of directory paths relative to the project root.
     * @throws RuntimeException if the directory does not exist.
     */
    public function listDirectories(string $path = '', bool $recursive = false) {
        $dirPath = $this->getAbsolutePath($path);
        if (!is_dir($dirPath)) {
            throw new RuntimeException("Directory does not exist: $dirPath");
        }

        $dirs = [];

        if ($recursive) {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($dirPath, \FilesystemIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::SELF_FIRST
            );

            foreach ($iterator as $item) {
                if ($item->isDir()) {
                    $dirs[] = $this->getRelativePath($item->getPathname());
                }
            }
        } else {
            $iterator = new \DirectoryIterator($dirPath);
            foreach ($iterator as $item) {
                if ($item->isDir() && !$item->isDot()) {
                    $dirs[] = $this->getRelativePath($item->getPathname());
                }
            }
        }

        return $dirs;
    }

    /**
     * Retrieve the contents of a file as a string.
     * 
     * @param string $filePath Path relative to the project root.
     * @return string The file contents.
     * @throws RuntimeException if the file does not exist or cannot be read.
     */
    public function getFileContent(string $filePath) {
        $absolutePath = $this->getAbsolutePath($filePath);
        if (!is_file($absolutePath) || !is_readable($absolutePath)) {
            throw new RuntimeException("File not readable: $absolutePath");
        }

        $content = file_get_contents($absolutePath);
        if ($content === false) {
            throw new RuntimeException("Failed to read file: $absolutePath");
        }

        return $content;
    }

    /**
     * Write content to a file. If the file doesn't exist, it will be created.
     * If it does exist, its contents will be overwritten.
     * 
     * @param string $filePath Path relative to the project root.
     * @param string $content  The content to write.
     * @return void
     * @throws RuntimeException if the file cannot be written.
     */
    public function putFileContent(string $filePath, string $content) {
        $absolutePath = $this->getAbsolutePath($filePath);

        // Ensure directory exists
        $dir = dirname($absolutePath);
        if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
            throw new RuntimeException("Failed to create directory: $dir");
        }

        if (file_put_contents($absolutePath, $content) === false) {
            throw new RuntimeException("Failed to write to file: $absolutePath");
        }
    }

    /**
     * Append content to a file. If the file doesn't exist, it will be created.
     * 
     * @param string $filePath Path relative to the project root.
     * @param string $content  Content to append.
     * @return void
     * @throws RuntimeException if the file cannot be written.
     */
    public function appendFileContent(string $filePath, string $content) {
        $absolutePath = $this->getAbsolutePath($filePath);

        // Ensure directory exists
        $dir = dirname($absolutePath);
        if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
            throw new RuntimeException("Failed to create directory: $dir");
        }

        if (file_put_contents($absolutePath, $content, FILE_APPEND) === false) {
            throw new RuntimeException("Failed to append to file: $absolutePath");
        }
    }

    /**
     * Delete a file.
     * 
     * @param string $filePath Path relative to the project root.
     * @return void
     * @throws RuntimeException if the file cannot be deleted.
     */
    public function deleteFile(string $filePath) {
        $absolutePath = $this->getAbsolutePath($filePath);
        if (is_file($absolutePath) && !unlink($absolutePath)) {
            throw new RuntimeException("Failed to delete file: $absolutePath");
        }
    }

    /**
     * Search for a given pattern (regular expression) within all files in a specified directory.
     * 
     * @param string $pattern  A PCRE pattern (e.g., '/function\s+(\w+)/').
     * @param string $path     Directory path relative to the project root to search in.
     * @param bool   $recursive Whether to search recursively.
     * @return array An associative array keyed by file path, each value is an array of matches.
     */
    public function searchInFiles(string $pattern, string $path = '', bool $recursive = true) {
        $matches = [];
        $files = $this->listFiles($path, $recursive);

        foreach ($files as $file) {
            $content = $this->getFileContent($file);
            preg_match_all($pattern, $content, $found, PREG_OFFSET_CAPTURE);
            if (!empty($found[0])) {
                $matches[$file] = $found;
            }
        }

        return $matches;
    }

    /**
     * Ensure a given directory exists, creating it if necessary.
     * 
     * @param string $path Path relative to the project root.
     * @param int    $mode Permissions for newly created directories.
     * @return void
     * @throws RuntimeException if the directory cannot be created.
     */
    public function ensureDirectoryExists(string $path, int $mode = 0777) {
        $absolutePath = $this->getAbsolutePath($path);
        if (!is_dir($absolutePath) && !mkdir($absolutePath, $mode, true)) {
            throw new RuntimeException("Failed to create directory: $absolutePath");
        }
    }

    /**
     * Convert a relative path (from project root) to an absolute path.
     * 
     * @param string $relativePath Relative path.
     * @return string Absolute path.
     */
    protected function getAbsolutePath(string $relativePath) {
        return $this->rootDir . DIRECTORY_SEPARATOR . ltrim($relativePath, DIRECTORY_SEPARATOR);
    }

    /**
     * Convert an absolute path to a path relative to the project root.
     * 
     * @param string $absolutePath Absolute path.
     * @return string Relative path.
     */
    protected function getRelativePath(string $absolutePath) {
        // Normalize slashes
        $root = rtrim(str_replace('\\', '/', $this->rootDir), '/');
        $abs = str_replace('\\', '/', $absolutePath);

        if (strpos($abs, $root) === 0) {
            return ltrim(substr($abs, strlen($root)), '/');
        }

        return $absolutePath;
    }

    /**
     * List all classes and their file paths for a given namespace.
     *
     * Uses the ReflectionNamespace class to find all classes within the given namespace
     * and, if desired, recursively searches sub-namespaces.
     *
     * @param string $namespace The namespace to reflect (e.g., "App\\Models").
     * @param bool   $recursive Whether to also include classes from sub-namespaces.
     * @return array  Associative array: fully qualified class name => file path.
     */
    public function listClassesInNamespace(string $namespace, bool $recursive = true) {
        $result = [];
        $refNs = new ReflectionNamespace($namespace);

        // Get all ReflectionClass objects owned by this namespace
        foreach ($refNs->getClasses() as $shortName => $reflectionClass) {
            // $reflectionClass is a ReflectionClass instance
            $fileName = $reflectionClass->getFileName();
            if ($fileName !== false) {
                // Convert to a relative path if it's inside the project root
                $fqcn = $reflectionClass->getName();
                $result[$fqcn] = $this->makePathRelative($fileName);
            }
        }

        if ($recursive) {
            // Get sub-namespaces and recurse
            foreach ($refNs->getNamespaces() as $subShortName => $subReflectionNamespace) {
                // $subReflectionNamespace is another ReflectionNamespace
                $subFqcn = $subReflectionNamespace->getName();
                $subResult = $this->listClassesInNamespace($subFqcn, true);
                $result = array_merge($result, $subResult);
            }
        }

        return $result;
    }

    /**
     * Convert an absolute path to a relative path (relative to project root).
     * If the file is outside the project root, returns the absolute path as-is.
     *
     * @param string $absolutePath
     * @return string
     */
    protected function makePathRelative(string $absolutePath) {
        // Normalize directory separators
        $root = rtrim(str_replace('\\', '/', $this->rootDir), '/');
        $path = str_replace('\\', '/', $absolutePath);

        if (strpos($path, $root) === 0) {
            return ltrim(substr($path, strlen($root)), '/');
        }

        return $absolutePath;
    }
}
