package com.psg.dlsysinfo.dl_sysinfo_server.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.MediaType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;

@RestController
@RequestMapping("/api/admin/scripts")
public class ScriptController {

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.username}")
    private String dbUser;

    @Value("${spring.datasource.password}")
    private String dbPass;

    @Value("${nvd.api.key}")
    private String apiKey;

    @Value("${nvd.max-range-days:7}")
    private String nvdMaxRangeDays;

    @Value("${scripts.directory:/home/sonder/ld-sysinfo-server/scripts}")
    private String scriptsDirectory;

    @Value("${scripts.logs.directory:/home/sonder/ld-sysinfo-server/scripts}")
    private String logsDirectory;

    private File getCveLogFile() {
        return new File(logsDirectory, "cve-sync.log");
    }

    private File getKevLogFile() {
        return new File(logsDirectory, "kev-sync.log");
    }

    private File getMsrcLogFile() {
        return new File(logsDirectory, "msrc-sync.log");
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-cve")
    public ResponseEntity<String> runCveScript(@AuthenticationPrincipal Object user) {
        return triggerScript("fetchCVE_v2.js", "📡 CVE enrichment sync launched (runs importCVEEnrichmentFast).", getCveLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-cve-backfill")
    public ResponseEntity<String> runCveBackfillScript(@AuthenticationPrincipal Object user) {
        return triggerScript("fetchCVE_withMORE.js", "📡 CVE backfill launched - will sync back to 2002.", getCveLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-kev")
    public ResponseEntity<String> runKevScript(@AuthenticationPrincipal Object user) {
        return triggerScript("fetchKEV.js", "📡 KEV sync launched in background.", getKevLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-msrc")
    public ResponseEntity<String> runMsrcScript(@AuthenticationPrincipal Object user) {
        return triggerScript("enrichCVE_MSRC.js", "📡 MSRC sync launched in background.", getMsrcLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/verify-cve-count")
    public ResponseEntity<String> verifyCveCount(@AuthenticationPrincipal Object user) {
        return triggerScript("verifyCVECountAPI.js", "🔍 CVE verification started - comparing with GitHub API.", getCveLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/fetch-cve/logs")
    public ResponseEntity<String> fetchLogs(@AuthenticationPrincipal Object user) {
        return readLogs(getCveLogFile());
    }

    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-cve/clear-logs")
    public ResponseEntity<String> clearLogs(@AuthenticationPrincipal Object user) {
        return clearLogs(getCveLogFile());
    }
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-kev/clear-logs")
    public ResponseEntity<String> clearKevLogs(@AuthenticationPrincipal Object user) {
        return clearLogs(getKevLogFile());
    }
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/fetch-msrc/clear-logs")
    public ResponseEntity<String> clearMsrcLogs(@AuthenticationPrincipal Object user) {
        return clearLogs(getMsrcLogFile());
    }


    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping(value = "/fetch-cve/logs/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamLogs(@AuthenticationPrincipal Object user) {
        Path logFile = getCveLogFile().toPath();

        return Flux.interval(Duration.ofSeconds(1))
                .map(tick -> {
                    try {
                        byte[] rawBytes = Files.readAllBytes(logFile);
                        String content = new String(rawBytes, StandardCharsets.UTF_8)
                                .replace("\r\n", "\n")
                                .replace("\r", "\n")
                                .replace("\uFEFF", "");
                        return content.isEmpty() ? "📭 No log output yet." : content;
                    } catch (IOException e) {
                        return "❌ Failed to read logs: " + e.getMessage();
                    }
                });
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping(value = "/fetch-kev/logs/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamKevLogs(@AuthenticationPrincipal Object user) {
        Path logFile = getKevLogFile().toPath();

        return Flux.<String>create(emitter -> {
            final long[] lastKnownPosition = {0};

            emitter.onRequest(n -> {
                try {
                    if (!Files.exists(logFile)) {
                        emitter.next("📭 Log file not found yet.");
                        return;
                    }

                    long fileSize = Files.size(logFile);

                    if (fileSize > lastKnownPosition[0]) {
                        try (RandomAccessFile raf = new RandomAccessFile(logFile.toFile(), "r")) {
                            raf.seek(lastKnownPosition[0]);
                            byte[] newBytes = new byte[(int) (fileSize - lastKnownPosition[0])];
                            raf.readFully(newBytes);
                            String newContent = new String(newBytes, StandardCharsets.UTF_8)
                                    .replace("\r\n", "\n")
                                    .replace("\r", "\n")
                                    .replace("\uFEFF", "");

                            emitter.next(newContent); // 👈 perfectly legal now
                            lastKnownPosition[0] = fileSize;
                        }
                    }
                } catch (Exception e) {
                    emitter.error(e);
                }
            });

            emitter.onDispose(() -> {
                System.out.println("Client disconnected from KEV stream.");
            });
        }).delayElements(Duration.ofSeconds(1));
    }




    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping(value = "/fetch-msrc/logs/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamMsrcLogs(@AuthenticationPrincipal Object user) {
        Path logFile = getMsrcLogFile().toPath();

        return Flux.interval(Duration.ofSeconds(1))
                .map(tick -> {
                    try {
                        byte[] rawBytes = Files.readAllBytes(logFile);
                        String content = new String(rawBytes, StandardCharsets.UTF_8)
                                .replace("\r\n", "\n")
                                .replace("\r", "\n")
                                .replace("\uFEFF", "");
                        return content.isEmpty() ? "📭 No log output yet." : content;
                    } catch (IOException e) {
                        return "❌ Failed to read logs: " + e.getMessage();
                    }
                });
    }


    private ResponseEntity<String> triggerScript(String scriptName, String message, File targetLogFile) {
        File scriptDir = new File(scriptsDirectory);
        File scriptFile = new File(scriptDir, scriptName);
        if (!scriptFile.exists()) {
            return ResponseEntity.status(404).body("❌ " + scriptName + " not found at: " + scriptFile.getAbsolutePath());
        }

        runNodeScript(scriptName, message, targetLogFile);
        return ResponseEntity.ok("🚀 " + message);
    }

    private void runNodeScript(String scriptName, String startMessage, File logTarget) {
        File scriptDir = new File(scriptsDirectory);
        File scriptFile = new File(scriptDir, scriptName);

        if (!scriptFile.exists()) {
            appendLog("❌ " + scriptName + " not found at: " + scriptFile.getAbsolutePath(), logTarget);
            return;
        }

        new Thread(() -> {
            try (PrintWriter logWriter = new PrintWriter(
                    new OutputStreamWriter(new FileOutputStream(logTarget, true), StandardCharsets.UTF_8), true)) {

                logWriter.println(formatNow() + " 🚀 " + startMessage);

                ProcessBuilder builder = new ProcessBuilder("node", scriptName);
                builder.directory(scriptDir);
                builder.redirectErrorStream(true);

                Map<String, String> env = builder.environment();
                env.put("DB_HOST", extractHost(dbUrl));
                env.put("DB_PORT", extractPort(dbUrl));
                env.put("DB_NAME", extractDbName(dbUrl));
                env.put("DB_USER", dbUser);
                env.put("DB_PASSWORD", dbPass);
                env.put("NVD_API_KEY", apiKey);
                env.put("NVD_MAX_RANGE_DAYS", nvdMaxRangeDays);

                env.put("NODE_OPTIONS", "--no-warnings --enable-source-maps");
                env.put("LC_ALL", "en_US.UTF-8");
                env.put("LANG", "en_US.UTF-8");
                env.put("LANGUAGE", "en_US:en");

                Process process = builder.start();

                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        logWriter.println(line);
                    }
                }

                int exitCode = process.waitFor();
                logWriter.println(formatNow() + " ✅ " + scriptName + " finished with exit code: " + exitCode);

            } catch (Exception e) {
                appendLog("❌ " + scriptName + " error: " + e.getMessage(), logTarget);
            }
        }).start();
    }

    private String extractHost(String url) {
        String clean = url.replace("jdbc:mysql://", "").split("/")[0];

        // Handle IPv6 addresses like [::1]:3307
        if (clean.startsWith("[")) {
            int closeBracket = clean.indexOf("]");
            if (closeBracket > 0) {
                return clean.substring(1, closeBracket);
            }
        }

        // Handle standard host:port format
        return clean.split(":")[0];
    }

    private String extractPort(String url) {
        String clean = url.replace("jdbc:mysql://", "").split("/")[0];

        // Handle IPv6: [::1]:3307
        if (clean.contains("]:")) {
            return clean.substring(clean.indexOf("]:") + 2);
        }

        // Handle standard: hostname:3307
        if (clean.contains(":") && !clean.startsWith("[")) {
            String[] parts = clean.split(":");
            if (parts.length > 1) {
                return parts[1];
            }
        }

        return "3306"; // default MySQL port
    }

    private String extractDbName(String url) {
        return url.substring(url.lastIndexOf("/") + 1).split("\\?")[0];
    }

    private void appendLog(String message, File logTarget) {
        try (PrintWriter logWriter = new PrintWriter(
                new OutputStreamWriter(new FileOutputStream(logTarget, true), StandardCharsets.UTF_8), true)) {
            logWriter.println(formatNow() + " " + message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ResponseEntity<String> readLogs(File logTarget) {
        if (!logTarget.exists()) {
            return ResponseEntity.ok("📭 No log file found yet.");
        }

        try {
            byte[] rawBytes = Files.readAllBytes(logTarget.toPath());
            String content = new String(rawBytes, StandardCharsets.UTF_8)
                    .replace("\r\n", "\n")
                    .replace("\r", "\n")
                    .replace("\uFEFF", "");
            return ResponseEntity.ok(content.isEmpty() ? "📭 Log is empty." : content);
        } catch (IOException e) {
            return ResponseEntity.status(500).body("❌ Error reading logs: " + e.getMessage());
        }
    }

    private ResponseEntity<String> clearLogs(File logTarget) {
        try {
            if (logTarget.exists()) {
                Files.delete(logTarget.toPath());
            }
            return ResponseEntity.ok("🧹 Logs cleared.");
        } catch (IOException e) {
            return ResponseEntity.status(500).body("❌ Failed to clear logs: " + e.getMessage());
        }
    }

    private String formatNow() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd LLL yyyy, hh:mm:ss a", Locale.ENGLISH)
                .withZone(ZoneId.systemDefault());
        return "[" + formatter.format(java.time.ZonedDateTime.now()).replace("AM", "am").replace("PM", "pm") + "]";
    }
}
