package com.cameleer.server.app.runtime;

import com.cameleer.common.model.LogEntry;
import com.cameleer.server.app.search.ClickHouseLogStore;
import com.cameleer.server.core.ingestion.BufferedLogEntry;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.model.Frame;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Streams Docker container stdout/stderr to ClickHouse until the agent registers via SSE.
 * One capture session per container, managed by container ID.
 */
public class ContainerLogForwarder {

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

    private static final int FLUSH_BATCH_SIZE = 50;
    private static final long FLUSH_INTERVAL_MS = 2_000;
    private static final long MAX_CAPTURE_DURATION_MS = 60_000;
    private static final long CLEANUP_INTERVAL_MS = 30_000;

    // Pattern to match Docker timestamp prefix: "2026-04-14T14:23:01.234567890Z "
    private static final Pattern DOCKER_TS_PATTERN = Pattern.compile(
            "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z)\\s(.*)$", Pattern.DOTALL);

    // Pattern to infer log level from Java log output
    private static final Pattern LEVEL_PATTERN = Pattern.compile(
            "\\b(ERROR|WARN|INFO|DEBUG|TRACE)\\b");

    private final DockerClient dockerClient;
    private final ClickHouseLogStore logStore;
    private final ConcurrentHashMap<String, CaptureSession> sessions = new ConcurrentHashMap<>();
    private final ExecutorService executor = Executors.newFixedThreadPool(10,
            r -> { Thread t = new Thread(r, "log-capture"); t.setDaemon(true); return t; });
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
            r -> { Thread t = new Thread(r, "log-capture-cleanup"); t.setDaemon(true); return t; });

    public ContainerLogForwarder(DockerClient dockerClient, ClickHouseLogStore logStore) {
        this.dockerClient = dockerClient;
        this.logStore = logStore;
        scheduler.scheduleAtFixedRate(this::cleanupExpiredSessions,
                CLEANUP_INTERVAL_MS, CLEANUP_INTERVAL_MS, TimeUnit.MILLISECONDS);
    }

    public void startCapture(String containerId, String instanceId, String appSlug, String envSlug, String tenantId) {
        if (sessions.containsKey(containerId)) {
            log.debug("Already capturing logs for container {}", containerId.substring(0, 12));
            return;
        }

        CaptureSession session = new CaptureSession(containerId, instanceId, appSlug, envSlug, tenantId);
        if (sessions.putIfAbsent(containerId, session) != null) {
            return; // another thread beat us
        }

        Future<?> future = executor.submit(() -> streamLogs(session));
        session.future = future;
        log.info("Started log capture for container {} (instance={}, app={}, env={})",
                containerId.substring(0, 12), instanceId, appSlug, envSlug);
    }

    public void stopCapture(String containerId) {
        CaptureSession session = sessions.remove(containerId);
        if (session == null) return;

        session.cancel();
        flushBuffer(session);
        log.info("Stopped log capture for container {} ({} lines captured)",
                containerId.substring(0, 12), session.lineCount);
    }

    @PreDestroy
    public void shutdown() {
        for (String containerId : new ArrayList<>(sessions.keySet())) {
            stopCapture(containerId);
        }
        scheduler.shutdownNow();
        executor.shutdownNow();
    }

    private void streamLogs(CaptureSession session) {
        try {
            session.callback = dockerClient.logContainerCmd(session.containerId)
                    .withFollowStream(true)
                    .withStdOut(true)
                    .withStdErr(true)
                    .withTimestamps(true)
                    .exec(new ResultCallback.Adapter<Frame>() {
                        @Override
                        public void onNext(Frame frame) {
                            if (session.cancelled) return;
                            String line = new String(frame.getPayload()).trim();
                            if (line.isEmpty()) return;

                            session.buffer.add(line);
                            session.lineCount++;

                            if (session.buffer.size() >= FLUSH_BATCH_SIZE
                                    || System.currentTimeMillis() - session.lastFlush >= FLUSH_INTERVAL_MS) {
                                flushBuffer(session);
                            }
                        }

                        @Override
                        public void onComplete() {
                            flushBuffer(session);
                            sessions.remove(session.containerId);
                            log.debug("Log stream completed for container {}",
                                    session.containerId.substring(0, 12));
                        }

                        @Override
                        public void onError(Throwable throwable) {
                            flushBuffer(session);
                            sessions.remove(session.containerId);
                            log.debug("Log stream error for container {}: {}",
                                    session.containerId.substring(0, 12), throwable.getMessage());
                        }
                    });
        } catch (Exception e) {
            sessions.remove(session.containerId);
            log.warn("Failed to start log capture for container {}: {}",
                    session.containerId.substring(0, 12), e.getMessage());
        }
    }

    private void flushBuffer(CaptureSession session) {
        List<String> lines;
        synchronized (session.buffer) {
            if (session.buffer.isEmpty()) return;
            lines = new ArrayList<>(session.buffer);
            session.buffer.clear();
        }
        session.lastFlush = System.currentTimeMillis();

        List<BufferedLogEntry> entries = new ArrayList<>(lines.size());
        for (String line : lines) {
            Instant timestamp = Instant.now();
            String message = line;

            Matcher tsMatcher = DOCKER_TS_PATTERN.matcher(line);
            if (tsMatcher.matches()) {
                try {
                    timestamp = Instant.parse(tsMatcher.group(1));
                } catch (DateTimeParseException e) {
                    // keep Instant.now()
                }
                message = tsMatcher.group(2);
            }

            String level = inferLevel(message);

            LogEntry logEntry = new LogEntry();
            logEntry.setTimestamp(timestamp);
            logEntry.setLevel(level);
            logEntry.setMessage(message);
            logEntry.setLoggerName("");
            logEntry.setThreadName("");
            logEntry.setStackTrace("");
            logEntry.setMdc(Collections.emptyMap());
            logEntry.setSource("container");

            entries.add(new BufferedLogEntry(
                    session.tenantId, session.envSlug, session.instanceId,
                    session.appSlug, logEntry));
        }

        try {
            logStore.insertBufferedBatch(entries);
        } catch (Exception e) {
            log.warn("Failed to flush {} container log entries for {}: {}",
                    entries.size(), session.appSlug, e.getMessage());
        }
    }

    private String inferLevel(String message) {
        if (message.startsWith("\tat ") || message.startsWith("Caused by:")) {
            return "ERROR";
        }
        Matcher m = LEVEL_PATTERN.matcher(message);
        if (m.find()) {
            return m.group(1);
        }
        return "INFO";
    }

    private void cleanupExpiredSessions() {
        long now = System.currentTimeMillis();
        for (Map.Entry<String, CaptureSession> entry : sessions.entrySet()) {
            CaptureSession session = entry.getValue();
            if (now - session.startedAt > MAX_CAPTURE_DURATION_MS) {
                log.info("Log capture timeout for container {} (app={}), stopping",
                        entry.getKey().substring(0, 12), session.appSlug);
                stopCapture(entry.getKey());
            }
        }
    }

    private static class CaptureSession {
        final String containerId;
        final String instanceId;
        final String appSlug;
        final String envSlug;
        final String tenantId;
        final long startedAt = System.currentTimeMillis();
        final List<String> buffer = Collections.synchronizedList(new ArrayList<>());
        volatile long lastFlush = System.currentTimeMillis();
        volatile long lineCount = 0;
        volatile boolean cancelled = false;
        volatile Future<?> future;
        volatile ResultCallback.Adapter<Frame> callback;

        CaptureSession(String containerId, String instanceId, String appSlug, String envSlug, String tenantId) {
            this.containerId = containerId;
            this.instanceId = instanceId;
            this.appSlug = appSlug;
            this.envSlug = envSlug;
            this.tenantId = tenantId;
        }

        void cancel() {
            cancelled = true;
            if (callback != null) {
                try { callback.close(); } catch (Exception e) { /* ignore */ }
            }
            if (future != null) {
                future.cancel(true);
            }
        }
    }
}
