package com.foxx.androidcast.dev;

import android.content.Context;
import android.util.Log;

import com.foxx.androidcast.network.CastProtocol;

import org.json.JSONObject;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * FR 0.1 — length-prefixed elementary stream capture while casting (dev-only).
 */
public final class StreamDumpWriter {
    private static final String TAG = "StreamDumpWriter";
    private static final int SCHEMA_VERSION = 1;
    private static final long MAX_BYTES = 100L * 1024L * 1024L;
    private static final long MAX_DURATION_MS = 5L * 60L * 1000L;

    private static final Object LOCK = new Object();
    private static StreamDumpWriter active;

    private final String sessionId;
    private final String datePrefix;
    private final long startedAtMs;
    private final File audioFile;
    private final File videoFile;
    private final File workDir;
    private BufferedOutputStream audioOut;
    private BufferedOutputStream videoOut;
    private final AtomicInteger audioFrames = new AtomicInteger();
    private final AtomicInteger videoFrames = new AtomicInteger();
    private final AtomicLong bytesWritten = new AtomicLong();
    private volatile boolean closed;

    private StreamDumpWriter(Context context) throws IOException {
        startedAtMs = System.currentTimeMillis();
        SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
        sessionId = fmt.format(new Date(startedAtMs));
        datePrefix = sessionId.substring(0, 8);
        workDir = new File(context.getFilesDir(), "stream_dumps");
        if (!workDir.exists() && !workDir.mkdirs()) {
            throw new IOException("cannot create stream_dumps dir");
        }
        audioFile = new File(workDir, datePrefix + "_audio_stream.raw");
        videoFile = new File(workDir, datePrefix + "_video_stream.raw");
        audioOut = new BufferedOutputStream(new FileOutputStream(audioFile));
        videoOut = new BufferedOutputStream(new FileOutputStream(videoFile));
    }

    public static void startIfEnabled(Context context) {
        if (!com.foxx.androidcast.AppPreferences.isDevStreamDumpEnabled(context)) {
            return;
        }
        synchronized (LOCK) {
            if (active != null) {
                return;
            }
            try {
                active = new StreamDumpWriter(context.getApplicationContext());
                Log.i(TAG, "recording " + active.sessionId);
            } catch (IOException e) {
                Log.w(TAG, "start failed: " + e.getMessage());
            }
        }
    }

    public static void finalizeIfActive(Context context) {
        StreamDumpWriter w;
        synchronized (LOCK) {
            w = active;
            active = null;
        }
        if (w != null) {
            w.closeAndZip(context.getApplicationContext());
        }
    }

    public static void recordFrame(byte type, byte[] payload) {
        StreamDumpWriter w = active;
        if (w == null || w.closed || payload == null) {
            return;
        }
        w.append(type, payload);
    }

    private void append(byte type, byte[] payload) {
        if (closed) {
            return;
        }
        if (bytesWritten.get() > MAX_BYTES
                || System.currentTimeMillis() - startedAtMs > MAX_DURATION_MS) {
            closed = true;
            return;
        }
        boolean audio = type == CastProtocol.MSG_AUDIO_FRAME || type == CastProtocol.MSG_AUDIO_CONFIG;
        boolean video = type == CastProtocol.MSG_VIDEO_FRAME;
        if (!audio && !video) {
            return;
        }
        byte[] frame = lengthPrefix(payload);
        synchronized (LOCK) {
            if (closed) {
                return;
            }
            try {
                if (audio) {
                    audioOut.write(frame);
                    audioFrames.incrementAndGet();
                } else {
                    videoOut.write(frame);
                    videoFrames.incrementAndGet();
                }
                bytesWritten.addAndGet(frame.length);
            } catch (IOException e) {
                Log.w(TAG, "write failed: " + e.getMessage());
                closed = true;
            }
        }
    }

    private static byte[] lengthPrefix(byte[] payload) {
        ByteBuffer buf = ByteBuffer.allocate(4 + payload.length);
        buf.putInt(payload.length);
        buf.put(payload);
        return buf.array();
    }

    private void closeAndZip(Context context) {
        synchronized (LOCK) {
            closed = true;
            closeQuietly(audioOut);
            closeQuietly(videoOut);
            audioOut = null;
            videoOut = null;
        }
        File zip = new File(workDir, sessionId + "_sess_record.zip");
        try {
            writeZip(zip);
            if (audioFile.exists()) {
                audioFile.delete();
            }
            if (videoFile.exists()) {
                videoFile.delete();
            }
            Log.i(TAG, "wrote " + zip.getAbsolutePath());
        } catch (IOException e) {
            Log.w(TAG, "zip failed: " + e.getMessage());
        }
    }

    private void writeZip(File zip) throws IOException {
        JSONObject meta = new JSONObject();
        try {
            meta.put("schema_version", SCHEMA_VERSION);
            meta.put("session_id", sessionId);
            meta.put("role", "sender");
            meta.put("record_mode", "raw_elementary");
            meta.put("started_at_epoch_ms", startedAtMs);
            meta.put("ended_at_epoch_ms", System.currentTimeMillis());
            JSONObject audio = new JSONObject();
            audio.put("file", audioFile.getName());
            audio.put("format", "length_prefixed");
            audio.put("frame_count", audioFrames.get());
            meta.put("audio", audio);
            JSONObject video = new JSONObject();
            video.put("file", videoFile.getName());
            video.put("format", "length_prefixed");
            video.put("frame_count", videoFrames.get());
            meta.put("video", video);
        } catch (org.json.JSONException e) {
            throw new IOException(e);
        }
        byte[] metaBytes;
        try {
            metaBytes = meta.toString(2).getBytes(java.nio.charset.StandardCharsets.UTF_8);
        } catch (org.json.JSONException e) {
            throw new IOException(e);
        }
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) {
            putEntry(zos, "meta.json", metaBytes);
            if (audioFrames.get() > 0 && audioFile.isFile()) {
                putFileEntry(zos, audioFile.getName(), audioFile);
            }
            if (videoFrames.get() > 0 && videoFile.isFile()) {
                putFileEntry(zos, videoFile.getName(), videoFile);
            }
        }
    }

    private static void putEntry(ZipOutputStream zos, String name, byte[] data) throws IOException {
        zos.putNextEntry(new ZipEntry(name));
        zos.write(data);
        zos.closeEntry();
    }

    private static void putFileEntry(ZipOutputStream zos, String name, File file) throws IOException {
        zos.putNextEntry(new ZipEntry(name));
        byte[] buf = new byte[8192];
        try (java.io.FileInputStream in = new java.io.FileInputStream(file)) {
            int n;
            while ((n = in.read(buf)) >= 0) {
                zos.write(buf, 0, n);
            }
        }
        zos.closeEntry();
    }

    private static void closeQuietly(BufferedOutputStream out) {
        if (out == null) {
            return;
        }
        try {
            out.flush();
            out.close();
        } catch (IOException ignored) {
        }
    }

    /** For unit tests. */
    static byte[] encodeLengthPrefixedFrame(byte[] payload) {
        return lengthPrefix(payload);
    }

    /** Exposed for tests — parse length-prefixed payload round-trip. */
    static int readLengthPrefix(byte[] framed) {
        if (framed.length < 4) {
            return -1;
        }
        return ByteBuffer.wrap(framed, 0, 4).getInt();
    }
}
