package cz.kamma.kfmanager.service;

import cz.kamma.kfmanager.model.FileItem;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.*;

/**
 * Service for file operations - copy, move, delete, etc.
 */
public class FileOperations {
    
    /**
    * Copy files/directories to target directory
     */
    public static void copy(List<FileItem> items, File targetDirectory, ProgressCallback callback) throws IOException {
        if (targetDirectory == null || !targetDirectory.isDirectory()) {
            throw new IOException("Target directory does not exist");
        }
        
        long totalSize = calculateTotalSize(items);
        final long[] currentCopied = {0};
        final OverwriteResponse[] globalResponse = {null};

        for (FileItem item : items) {
            if (callback != null && callback.isCancelled()) break;
            File source = item.getFile();
            File target = new File(targetDirectory, source.getName());

            // If target is the same as source (copying to the same directory), rename target
            if (source.getAbsolutePath().equals(target.getAbsolutePath())) {
                target = new File(targetDirectory, "copy-of-" + source.getName());
            } else if (target.exists()) {
                if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
                if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
                    OverwriteResponse res = callback.confirmOverwrite(target);
                    if (res == OverwriteResponse.CANCEL) break;
                    if (res == OverwriteResponse.NO_TO_ALL) {
                        globalResponse[0] = OverwriteResponse.NO_TO_ALL;
                        continue;
                    }
                    if (res == OverwriteResponse.NO) continue;
                    if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
                }
            }
            
            if (source.isDirectory()) {
                copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback, globalResponse);
            } else {
                copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
            }
        }
    }

    private static long calculateTotalSize(List<FileItem> items) {
        long total = 0;
        for (FileItem item : items) {
            total += calculateSize(item.getFile().toPath());
        }
        return total;
    }

    private static long calculateSize(Path path) {
        if (!Files.exists(path)) return 0;
        if (!Files.isDirectory(path)) {
            try { return Files.size(path); } catch (IOException e) { return 0; }
        }
        final long[] size = {0};
        try {
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    size[0] += attrs.size();
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException ignore) {}
        return size[0];
    }

    private static void copyFileWithProgress(Path source, Path target, long totalSize, long[] totalCopied, ProgressCallback callback) throws IOException {
        try (InputStream in = Files.newInputStream(source);
             OutputStream out = Files.newOutputStream(target)) {
            byte[] buffer = new byte[8192];
            int length;
            while ((length = in.read(buffer)) > 0) {
                if (callback != null && callback.isCancelled()) return;
                out.write(buffer, 0, length);
                totalCopied[0] += length;
                if (callback != null) {
                    callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString());
                }
            }
        }
        Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
    }

    private static void copyDirectory(Path source, Path target, long totalSize, final long[] totalCopied, ProgressCallback callback, final OverwriteResponse[] globalResponse) throws IOException {
        Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                Path targetDir = target.resolve(source.relativize(dir));
                Files.createDirectories(targetDir);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
                Path targetFile = target.resolve(source.relativize(file));

                if (Files.exists(targetFile)) {
                    if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
                    if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
                        OverwriteResponse res = callback.confirmOverwrite(targetFile.toFile());
                        if (res == OverwriteResponse.CANCEL) return FileVisitResult.TERMINATE;
                        if (res == OverwriteResponse.NO_TO_ALL) {
                            globalResponse[0] = OverwriteResponse.NO_TO_ALL;
                            return FileVisitResult.CONTINUE;
                        }
                        if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
                        if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
                    }
                }

                copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback);
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    /**
    * Move files/directories to target directory
     */
    public static void move(List<FileItem> items, File targetDirectory, ProgressCallback callback) throws IOException {
        if (targetDirectory == null || !targetDirectory.isDirectory()) {
            throw new IOException("Target directory does not exist");
        }
        
        int current = 0;
        int total = items.size();
        final OverwriteResponse[] globalResponse = {null};
        
        for (FileItem item : items) {
            if (callback != null && callback.isCancelled()) break;
            current++;
            File source = item.getFile();
            File target = new File(targetDirectory, source.getName());
            
            if (target.exists() && !source.getAbsolutePath().equals(target.getAbsolutePath())) {
                if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
                if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
                    OverwriteResponse res = callback.confirmOverwrite(target);
                    if (res == OverwriteResponse.CANCEL) break;
                    if (res == OverwriteResponse.NO_TO_ALL) {
                        globalResponse[0] = OverwriteResponse.NO_TO_ALL;
                        continue;
                    }
                    if (res == OverwriteResponse.NO) continue;
                    if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
                }
            }

            if (callback != null) {
                callback.onProgress(current, total, source.getName());
            }
            
            Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
        }
    }
    
    /**
    * Delete files/directories
     */
    public static void delete(List<FileItem> items, ProgressCallback callback) throws IOException {
        int current = 0;
        int total = items.size();
        
        for (FileItem item : items) {
            if (callback != null && callback.isCancelled()) break;
            current++;
            File file = item.getFile();
            
            if (callback != null) {
                callback.onProgress(current, total, file.getName());
            }
            
            if (file.isDirectory()) {
                deleteDirectory(file.toPath());
            } else {
                Files.delete(file.toPath());
            }
        }
    }
    
    /**
    * Rename a file or directory
     */
    public static void rename(File file, String newName) throws IOException {
        File target = new File(file.getParentFile(), newName);
        Files.move(file.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
    }
    
    /**
    * Create a new directory
     */
    public static void createDirectory(File parentDirectory, String name) throws IOException {
        File newDir = new File(parentDirectory, name);
        if (!newDir.mkdir()) {
            throw new IOException("Failed to create directory");
        }
    }
    
    /**
    * Delete directory recursively
     */
    private static void deleteDirectory(Path directory) throws IOException {
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }
            
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    /**
    * Search files by pattern
     */
    public static void search(File directory, String pattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
        if (pattern == null) return;
        // Prepare a compiled regex if the pattern contains wildcards to avoid recompiling per-file
        Pattern filenameRegex = null;
        if (pattern.contains("*") || pattern.contains("?")) {
            String regex = pattern
                    .replace(".", "\\.")
                    .replace("*", ".*")
                    .replace("?", ".");
            filenameRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
        }
        searchRecursive(directory.toPath(), pattern.toLowerCase(), filenameRegex, recursive, searchArchives, callback);
    }

    /**
     * Search file contents for a text fragment (case-insensitive).
     * Calls callback.onFileFound(file) when a file contains the text.
     */
    public static void searchContents(File directory, String text, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
        if (text == null) return;
        // Precompile a case-insensitive pattern for content search to avoid per-line lowercasing
        Pattern contentPattern = Pattern.compile(Pattern.quote(text), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
        searchContentsRecursive(directory.toPath(), contentPattern, recursive, searchArchives, callback);
    }
    
    private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
            for (Path entry : stream) {
                if (Files.isDirectory(entry)) {
                    if (recursive) {
                        searchRecursive(entry, patternLower, filenameRegex, recursive, searchArchives, callback);
                    }
                } else {
                    File file = entry.toFile();
                    String fileName = file.getName();
                    String fileNameLower = fileName.toLowerCase();
                    boolean matched = false;
                    if (fileNameLower.contains(patternLower)) matched = true;
                    else if (filenameRegex != null) {
                        Matcher m = filenameRegex.matcher(fileName);
                        if (m.matches()) matched = true;
                    }
                    if (matched) {
                        callback.onFileFound(file, null);
                    }

                    // SEARCH IN ARCHIVES
                    if (searchArchives && isArchiveFile(file)) {
                        searchInArchive(file, fileNameLower, filenameRegex, callback);
                    }
                }
            }
        } catch (AccessDeniedException e) {
            // Ignore directories without access
        }
    }

    private static void searchContentsRecursive(Path directory, Pattern contentPattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
            for (Path entry : stream) {
                if (Files.isDirectory(entry)) {
                    if (recursive) {
                        searchContentsRecursive(entry, contentPattern, recursive, searchArchives, callback);
                    }
                } else {
                    File file = entry.toFile();
                    // Try reading file as text line-by-line and search for pattern (case-insensitive via compiled Pattern)
                    try (BufferedReader br = Files.newBufferedReader(entry)) {
                        String line;
                        boolean found = false;
                        while ((line = br.readLine()) != null) {
                            if (contentPattern.matcher(line).find()) {
                                found = true;
                                break;
                            }
                        }
                        if (found) callback.onFileFound(file, null);
                    } catch (IOException ex) {
                        // Skip files that cannot be read as text
                    }

                    // SEARCH IN ARCHIVES CONTENTS
                    if (searchArchives && isArchiveFile(file)) {
                        searchContentsInArchive(file, contentPattern, callback);
                    }
                }
            }
        } catch (AccessDeniedException e) {
            // Ignore directories without access
        }
    }

    private static boolean isArchiveFile(File f) {
        if (f == null) return false;
        String n = f.getName().toLowerCase();
        return n.endsWith(".zip") || n.endsWith(".jar") || n.endsWith(".tar") || n.endsWith(".tar.gz") || n.endsWith(".tgz") || n.endsWith(".7z") || n.endsWith(".rar");
    }

    private static void searchInArchive(File archive, String patternLower, Pattern filenameRegex, SearchCallback callback) {
        String name = archive.getName().toLowerCase();
        try {
            if (name.endsWith(".zip") || name.endsWith(".jar")) {
                try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
                    ZipEntry entry;
                    while ((entry = zis.getNextEntry()) != null) {
                        if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                        }
                    }
                }
            } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
                InputStream is = new FileInputStream(archive);
                if (name.endsWith(".gz") || name.endsWith(".tgz")) {
                    is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
                }
                try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
                    org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
                    while ((entry = tais.getNextTarEntry()) != null) {
                        if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                        }
                    }
                }
            } else if (name.endsWith(".7z")) {
                try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
                    org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
                    while ((entry = sevenZFile.getNextEntry()) != null) {
                        if (matchEntry(entry.getName(), patternLower, filenameRegex)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                        }
                    }
                }
            } else if (name.endsWith(".rar")) {
                try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
                    for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
                        if (matchEntry(fh.getFileName(), patternLower, filenameRegex)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + fh.getFileName());
                        }
                    }
                }
            }
        } catch (Exception ignore) {}
    }

    private static void searchContentsInArchive(File archive, Pattern contentPattern, SearchCallback callback) {
        String name = archive.getName().toLowerCase();
        try {
            if (name.endsWith(".zip") || name.endsWith(".jar")) {
                try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
                    ZipEntry entry;
                    while ((entry = zis.getNextEntry()) != null) {
                        if (!entry.isDirectory() && searchInStream(zis, contentPattern)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                        }
                    }
                }
            } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
                InputStream is = new FileInputStream(archive);
                if (name.endsWith(".gz") || name.endsWith(".tgz")) {
                    is = new org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream(is);
                }
                try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) {
                    org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
                    while ((entry = tais.getNextTarEntry()) != null) {
                        if (!entry.isDirectory() && searchInStream(tais, contentPattern)) {
                            callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                        }
                    }
                }
            } else if (name.endsWith(".7z")) {
                try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
                    org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
                    while ((entry = sevenZFile.getNextEntry()) != null) {
                        if (!entry.isDirectory()) {
                            // SevenZFile.read(buffer) reads from current entry
                            if (searchInStream(new InputStream() {
                                @Override
                                public int read() throws IOException {
                                    return sevenZFile.read();
                                }
                                @Override
                                public int read(byte[] b, int off, int len) throws IOException {
                                    return sevenZFile.read(b, off, len);
                                }
                            }, contentPattern)) {
                                callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + entry.getName());
                            }
                        }
                    }
                }
            } else if (name.endsWith(".rar")) {
                try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
                    for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
                        if (!fh.isDirectory()) {
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            rar.extractFile(fh, baos);
                            if (contentPattern.matcher(new String(baos.toByteArray())).find()) {
                                callback.onFileFound(archive, archive.getAbsolutePath() + File.separator + fh.getFileName());
                            }
                        }
                    }
                }
            }
        } catch (Exception ignore) {}
    }

    private static boolean matchEntry(String entryName, String patternLower, Pattern filenameRegex) {
        if (entryName == null) return false;
        String nameShort = entryName;
        int lastSlash = entryName.lastIndexOf('/');
        if (lastSlash != -1) nameShort = entryName.substring(lastSlash + 1);
        
        String nameLower = nameShort.toLowerCase();
        if (nameLower.contains(patternLower)) return true;
        if (filenameRegex != null && filenameRegex.matcher(nameShort).matches()) return true;
        return false;
    }

    private static boolean searchInStream(InputStream is, Pattern contentPattern) {
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String line;
            while ((line = br.readLine()) != null) {
                if (contentPattern.matcher(line).find()) {
                    return true;
                }
            }
        } catch (Exception ignore) {}
        return false;
    }
    
    /**
     * Zip files/directories into a target zip file
     */
    public static void zip(List<FileItem> items, File targetZipFile, ProgressCallback callback) throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(targetZipFile))) {
            long current = 0;
            long total = items.size();
            
            for (FileItem item : items) {
                current++;
                File source = item.getFile();
                if (callback != null) {
                    callback.onProgress(current, total, source.getName());
                }
                addToZip(source, source.getName(), zos);
            }
        }
    }

    private static void addToZip(File fileToZip, String fileName, ZipOutputStream zos) throws IOException {
        if (fileToZip.isHidden()) {
            return;
        }
        if (fileToZip.isDirectory()) {
            if (fileName.endsWith("/")) {
                zos.putNextEntry(new ZipEntry(fileName));
                zos.closeEntry();
            } else {
                zos.putNextEntry(new ZipEntry(fileName + "/"));
                zos.closeEntry();
            }
            File[] children = fileToZip.listFiles();
            if (children != null) {
                for (File childFile : children) {
                    addToZip(childFile, fileName + "/" + childFile.getName(), zos);
                }
            }
            return;
        }
        try (FileInputStream fis = new FileInputStream(fileToZip)) {
            ZipEntry zipEntry = new ZipEntry(fileName);
            zos.putNextEntry(zipEntry);
            byte[] bytes = new byte[1024];
            int length;
            while ((length = fis.read(bytes)) >= 0) {
                zos.write(bytes, 0, length);
            }
            zos.closeEntry();
        }
    }
    
    /**
     * Unzip a zip file into a target directory
     */
    public static void unzip(File zipFile, File targetDirectory, ProgressCallback callback) throws IOException {
        if (!targetDirectory.exists()) {
            Files.createDirectories(targetDirectory.toPath());
        }

        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
            ZipEntry entry;
            // First pass or estimated count could be done, but keep it simple for now
            while ((entry = zis.getNextEntry()) != null) {
                File newFile = new File(targetDirectory, entry.getName());
                
                if (callback != null) {
                    callback.onProgress(0L, 0L, entry.getName());
                }

                if (entry.isDirectory()) {
                    if (!newFile.isDirectory() && !newFile.mkdirs()) {
                        throw new IOException("Failed to create directory " + newFile);
                    }
                } else {
                    // create parent directories if they don't exist
                    File parent = newFile.getParentFile();
                    if (!parent.isDirectory() && !parent.mkdirs()) {
                        throw new IOException("Failed to create directory " + parent);
                    }
                    
                    // write file content
                    try (FileOutputStream fos = new FileOutputStream(newFile)) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, len);
                        }
                    }
                }
                zis.closeEntry();
            }
        }
    }
    
    // legacy matchesPattern removed — filename wildcard handling is done via a precompiled Pattern
    
    /**
     * Callback for operation progress
     */
    public enum OverwriteResponse {
        YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL
    }

    public interface ProgressCallback {
        void onProgress(long current, long total, String currentFile);
        default boolean isCancelled() { return false; }
        default OverwriteResponse confirmOverwrite(File file) { return OverwriteResponse.YES; }
    }
    
    /**
     * Callback for search
     */
    public interface SearchCallback {
        void onFileFound(File file, String virtualPath);
    }
}
