package com.cfdeployer.service;

import com.cfdeployer.model.CfDeployRequest;
import com.cfdeployer.model.CfDeployResponse;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class CfCliService {

    private static final Logger log = LoggerFactory.getLogger(CfCliService.class);

    @Value("${cf.cli.timeout:600}")
    private long timeout;

    @Value("${cf.cli.path:}")
    private String cfCliPath;

    public CfDeployResponse deployApplication(CfDeployRequest request, MultipartFile jarFile, MultipartFile manifest) {
        Path tempDir = null;
        try {
            log.info("=== Starting deployment for app: {} ===", request.getAppName());
            log.info("Target: {}/{}/{}", request.getApiEndpoint(), request.getOrganization(), request.getSpace());

            tempDir = Files.createTempDirectory("cf-deploy-");
            log.info("Created temporary directory: {}", tempDir);

            Path jarPath = tempDir.resolve(jarFile.getOriginalFilename());
            Path manifestPath = tempDir.resolve("manifest.yml");

            log.debug("Copying uploaded files - JAR: {}, Manifest: {}", jarFile.getOriginalFilename(), manifest.getOriginalFilename());
            log.debug("JAR file size: {} bytes", jarFile.getSize());
            log.debug("Manifest file size: {} bytes", manifest.getSize());

            Files.copy(jarFile.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING);
            Files.copy(manifest.getInputStream(), manifestPath, StandardCopyOption.REPLACE_EXISTING);

            log.info("Successfully copied JAR: {} ({} bytes) to {}", jarFile.getOriginalFilename(), jarFile.getSize(), jarPath);
            log.info("Successfully copied manifest to {}", manifestPath);
            log.debug("JAR absolute path: {}", jarPath.toAbsolutePath());
            log.debug("JAR file exists: {}", Files.exists(jarPath));
            log.debug("JAR file is readable: {}", Files.isReadable(jarPath));

            StringBuilder output = new StringBuilder();

            login(request, output);
            pushApplication(request, tempDir, jarPath, output);
            logout(output);

            log.info("=== Deployment completed successfully for app: {} ===", request.getAppName());
            return CfDeployResponse.success(output.toString());

        } catch (Exception e) {
            log.error("=== Deployment failed for app: {} ===", request.getAppName());
            log.error("Error type: {}", e.getClass().getName());
            log.error("Error message: {}", e.getMessage(), e);
            return CfDeployResponse.failure(e.getMessage(), e.toString());
        } finally {
            if (tempDir != null) {
                cleanupTempDirectory(tempDir);
            }
        }
    }

    private void login(CfDeployRequest request, StringBuilder output) throws Exception {
        log.info("Logging into Cloud Foundry at: {}", request.getApiEndpoint());

        List<String> command = new ArrayList<>();
        command.add(getCfCliExecutable());
        command.add("login");
        command.add("-a");
        command.add(request.getApiEndpoint());
        command.add("-u");
        command.add(request.getUsername());
        command.add("-p");
        command.add(request.getPassword());
        command.add("-o");
        command.add(request.getOrganization());
        command.add("-s");
        command.add(request.getSpace());

        if (Boolean.TRUE.equals(request.getSkipSslValidation())) {
            command.add("--skip-ssl-validation");
        }

        executeCommand(command, output, true);
        log.info("Successfully logged into Cloud Foundry");
    }

    private void pushApplication(CfDeployRequest request, Path workingDir, Path jarPath, StringBuilder output) throws Exception {
        log.info("Pushing application: {}", request.getAppName());

        List<String> command = new ArrayList<>();
        command.add(getCfCliExecutable());
        command.add("push");
        command.add(request.getAppName());
        command.add("-f");
        command.add("manifest.yml");
        command.add("-p");
        command.add(jarPath.toAbsolutePath().toString());

        executeCommand(command, output, false, workingDir.toFile());
        log.info("Successfully pushed application: {}", request.getAppName());
    }

    private void logout(StringBuilder output) throws Exception {
        log.info("Logging out from Cloud Foundry");

        List<String> command = new ArrayList<>();
        command.add(getCfCliExecutable());
        command.add("logout");

        executeCommand(command, output, false);
        log.info("Successfully logged out from Cloud Foundry");
    }

    private void executeCommand(List<String> command, StringBuilder output, boolean maskPassword) throws Exception {
        executeCommand(command, output, maskPassword, null);
    }

    private void executeCommand(List<String> command, StringBuilder output, boolean maskPassword, File workingDir) throws Exception {
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        if (workingDir != null) {
            processBuilder.directory(workingDir);
            log.debug("Working directory: {}", workingDir.getAbsolutePath());
        }
        processBuilder.redirectErrorStream(true);

        if (maskPassword) {
            List<String> maskedCommand = new ArrayList<>(command);
            for (int i = 0; i < maskedCommand.size(); i++) {
                if ("-p".equals(maskedCommand.get(i)) && i + 1 < maskedCommand.size()) {
                    maskedCommand.set(i + 1, "********");
                }
            }
            log.info("Executing CF CLI command: {}", String.join(" ", maskedCommand));
        } else {
            log.info("Executing CF CLI command: {}", String.join(" ", command));
        }

        long startTime = System.currentTimeMillis();
        Process process = processBuilder.start();
        log.debug("Process started with PID: {}", process.pid());

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            int lineCount = 0;
            while ((line = reader.readLine()) != null) {
                lineCount++;
                output.append(line).append("\n");
                log.info("CF CLI: {}", line);
            }
            log.debug("Command produced {} lines of output", lineCount);
        }

        boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
        long duration = System.currentTimeMillis() - startTime;

        if (!finished) {
            log.error("Command execution timed out after {} seconds", timeout);
            process.destroyForcibly();
            throw new RuntimeException("Command execution timed out after " + timeout + " seconds");
        }

        int exitCode = process.exitValue();
        log.info("Command completed in {}ms with exit code: {}", duration, exitCode);

        if (exitCode != 0) {
            log.error("Command failed with exit code: {}", exitCode);
            log.error("Command output:\n{}", output.toString());
            throw new RuntimeException("Command failed with exit code: " + exitCode);
        }
    }

    private String getCfCliExecutable() throws IOException {
        if (cfCliPath != null && !cfCliPath.isEmpty()) {
            log.info("Using custom CF CLI path: {}", cfCliPath);
            return cfCliPath;
        }

        String os = getOperatingSystem();
        // CF CLI v8 uses cf8/cf8.exe as the executable name
        String executable = os.equals("windows") ? "cf8.exe" : "cf8";

        log.info("Detected operating system: {}", os);
        log.info("CF CLI executable name: {}", executable);

        String userDir = System.getProperty("user.dir");
        log.info("User directory (user.dir): {}", userDir);

        // In Spring Boot fat JAR on Tanzu, the structure is:
        // /home/vcap/app/BOOT-INF/classes/cf-cli/linux/cf8
        Path directPath = Path.of(userDir, "BOOT-INF", "classes", "cf-cli", os, executable);
        log.info("Checking for CF CLI at direct file path: {}", directPath);

        if (Files.exists(directPath) && Files.isReadable(directPath)) {
            log.info("Found CF CLI binary at direct file path: {} ({} bytes)",
                    directPath, Files.size(directPath));

            File tempFile = File.createTempFile("cf-cli-", os.equals("windows") ? ".exe" : "");
            tempFile.deleteOnExit();
            log.info("Created temp file: {}", tempFile.getAbsolutePath());

            // Copy from direct file path to temp file
            long bytesCopied = Files.copy(directPath, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            log.info("Copied CF CLI to temp file: {} ({} bytes)", tempFile.getAbsolutePath(), bytesCopied);

            if (bytesCopied == 0) {
                throw new IOException("CF CLI copy failed - 0 bytes copied from " + directPath);
            }

            if (!os.equals("windows")) {
                try {
                    Set<PosixFilePermission> perms = new HashSet<>();
                    perms.add(PosixFilePermission.OWNER_READ);
                    perms.add(PosixFilePermission.OWNER_WRITE);
                    perms.add(PosixFilePermission.OWNER_EXECUTE);
                    Files.setPosixFilePermissions(tempFile.toPath(), perms);
                    log.info("Set executable permissions on CF CLI binary using Java NIO");
                } catch (Exception e) {
                    log.warn("Failed to set permissions using Java NIO, trying chmod: {}", e.getMessage());
                    try {
                        ProcessBuilder pb = new ProcessBuilder("chmod", "+x", tempFile.getAbsolutePath());
                        Process p = pb.start();
                        int exitCode = p.waitFor();
                        if (exitCode == 0) {
                            log.info("Successfully set executable permissions using chmod");
                        } else {
                            log.error("chmod failed with exit code: {}", exitCode);
                        }
                    } catch (Exception ex) {
                        log.error("Failed to set executable permissions: {}", ex.getMessage(), ex);
                    }
                }
            }

            log.info("Successfully prepared CF CLI executable: {}", tempFile.getAbsolutePath());
            log.info("File exists: {}, Can read: {}, Can execute: {}",
                    tempFile.exists(), tempFile.canRead(), tempFile.canExecute());

            return tempFile.getAbsolutePath();
        }

        // Fallback to classpath resource loading (for local development)
        log.info("Direct file path not found, falling back to classpath resource loading");
        String resourcePath = "cf-cli/" + os + "/" + executable;
        log.info("Looking for CF CLI at classpath resource: {}", resourcePath);

        ClassPathResource resource = new ClassPathResource(resourcePath);

        if (!resource.exists()) {
            log.error("CF CLI binary not found at direct path: {}", directPath);
            log.error("CF CLI binary not found in classpath: {}", resourcePath);
            throw new IOException("CF CLI binary not found for OS: " + os);
        }

        log.info("Found CF CLI in classpath, extracting...");

        File tempFile = File.createTempFile("cf-cli-", os.equals("windows") ? ".exe" : "");
        tempFile.deleteOnExit();
        log.info("Created temp file: {}", tempFile.getAbsolutePath());

        try (InputStream inputStream = resource.getInputStream()) {
            long bytesCopied = Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            log.info("CF CLI extracted to temp file: {} ({} bytes)", tempFile.getAbsolutePath(), bytesCopied);

            if (bytesCopied == 0) {
                throw new IOException("CF CLI extraction failed - 0 bytes copied");
            }
        }

        if (!os.equals("windows")) {
            try {
                Set<PosixFilePermission> perms = new HashSet<>();
                perms.add(PosixFilePermission.OWNER_READ);
                perms.add(PosixFilePermission.OWNER_WRITE);
                perms.add(PosixFilePermission.OWNER_EXECUTE);
                Files.setPosixFilePermissions(tempFile.toPath(), perms);
                log.info("Set executable permissions on CF CLI binary");
            } catch (Exception e) {
                log.warn("Failed to set permissions: {}", e.getMessage());
            }
        }

        log.info("Successfully prepared CF CLI executable: {}", tempFile.getAbsolutePath());
        return tempFile.getAbsolutePath();
    }

    private String getOperatingSystem() {
        String osName = System.getProperty("os.name").toLowerCase();
        log.debug("System OS name property: {}", osName);
        if (osName.contains("win")) {
            return "windows";
        } else if (osName.contains("mac")) {
            return "macos";
        } else {
            return "linux";
        }
    }

    private void cleanupTempDirectory(Path tempDir) {
        try {
            Files.walk(tempDir)
                    .sorted(Comparator.reverseOrder())
                    .forEach(path -> {
                        try {
                            Files.delete(path);
                        } catch (IOException e) {
                            log.warn("Failed to delete file: {}", path, e);
                        }
                    });
            log.debug("Cleaned up temporary directory: {}", tempDir);
        } catch (IOException e) {
            log.warn("Failed to clean up temporary directory: {}", tempDir, e);
        }
    }

    public String listApps(CfDeployRequest request) {
        try {
            log.info("Listing applications for org: {}, space: {}", request.getOrganization(), request.getSpace());
            StringBuilder output = new StringBuilder();

            login(request, output);

            List<String> command = new ArrayList<>();
            command.add(getCfCliExecutable());
            command.add("apps");

            executeCommand(command, output, false);
            logout(output);

            log.info("Successfully retrieved apps list");
            return output.toString();
        } catch (Exception e) {
            log.error("Failed to list apps", e);
            throw new RuntimeException("Failed to list apps: " + e.getMessage(), e);
        }
    }

    public String listRoutes(CfDeployRequest request) {
        try {
            log.info("Listing routes for org: {}, space: {}", request.getOrganization(), request.getSpace());
            StringBuilder output = new StringBuilder();

            login(request, output);

            List<String> command = new ArrayList<>();
            command.add(getCfCliExecutable());
            command.add("routes");

            executeCommand(command, output, false);
            logout(output);

            log.info("Successfully retrieved routes list");
            return output.toString();
        } catch (Exception e) {
            log.error("Failed to list routes", e);
            throw new RuntimeException("Failed to list routes: " + e.getMessage(), e);
        }
    }

    public String getAppDetails(CfDeployRequest request, String appName) {
        try {
            log.info("Getting details for app: {} in org: {}, space: {}", appName, request.getOrganization(), request.getSpace());
            StringBuilder output = new StringBuilder();

            login(request, output);

            List<String> command = new ArrayList<>();
            command.add(getCfCliExecutable());
            command.add("app");
            command.add(appName);

            executeCommand(command, output, false);
            logout(output);

            log.info("Successfully retrieved app details for: {}", appName);
            return output.toString();
        } catch (Exception e) {
            log.error("Failed to get app details for: {}", appName, e);
            throw new RuntimeException("Failed to get app details: " + e.getMessage(), e);
        }
    }

    public String getAppLogs(CfDeployRequest request, String appName, boolean recent) {
        try {
            log.info("Getting {} logs for app: {}", recent ? "recent" : "tail", appName);
            StringBuilder output = new StringBuilder();

            login(request, output);

            List<String> command = new ArrayList<>();
            command.add(getCfCliExecutable());
            command.add("logs");
            command.add(appName);
            if (recent) {
                command.add("--recent");
            }

            executeCommand(command, output, false);
            logout(output);

            log.info("Successfully retrieved logs for: {}", appName);
            return output.toString();
        } catch (Exception e) {
            log.error("Failed to get logs for: {}", appName, e);
            throw new RuntimeException("Failed to get app logs: " + e.getMessage(), e);
        }
    }
}
