use crate::define::{system_time_to_millis, system_time_to_yyyymmddhhmmss};
use crate::idx_line::IdxLine;
use anyhow::{Context, Result, anyhow};
use std::fs::{File, OpenOptions, create_dir_all, read_dir};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::time::SystemTime;

// BlobWriter handles writing bytes to a blob (Write + Read + Seek)
pub struct BlobWriter<W: Write + Read + Seek> {
    inner: W,
    pos: u64,
}

impl<W: Write + Read + Seek> BlobWriter<W> {
    pub fn new(inner: W) -> Self {
        Self { inner, pos: 0 }
    }

    /// append data to blob, return offset before write
    pub fn write_blob(&mut self, data: &[u8]) -> Result<u64> {
        let offset = self.pos;
        self.inner
            .write_all(data)
            .with_context(|| "failed to write to blob writer")?;
        self.inner
            .flush()
            .with_context(|| "failed to flush blob writer")?;
        self.pos = self.pos.saturating_add(data.len() as u64);
        Ok(offset)
    }

    /// Read all blob bytes
    pub fn read_all(&mut self) -> Result<Vec<u8>> {
        self.inner
            .seek(SeekFrom::Start(0))
            .with_context(|| "failed to seek blob to start for read_all")?;
        let mut blob_vec = Vec::new();
        self.inner
            .read_to_end(&mut blob_vec)
            .with_context(|| "failed to read blob bytes in read_all")?;
        Ok(blob_vec)
    }

    pub fn close(mut self) -> Result<()> {
        let _ = self.inner.flush();
        Ok(())
    }
}

// IdxWriter handles writing idx records (16 bytes)
pub struct IdxWriter<I: Write + Read + Seek> {
    inner: I,
}

impl<I: Write + Read + Seek> IdxWriter<I> {
    pub fn new(inner: I) -> Self {
        Self { inner }
    }

    pub fn write_idx(&mut self, ts_millis: u64, offset: u64) -> Result<()> {
        let line = IdxLine::new(ts_millis, offset);
        let mut buf = [0u8; 16];
        line.to_bytes(&mut buf)?;
        self.inner
            .write_all(&buf)
            .with_context(|| "failed to write idx record")?;
        self.inner
            .flush()
            .with_context(|| "failed to flush idx writer")?;
        Ok(())
    }

    /// Read all idx bytes
    pub fn read_all(&mut self) -> Result<Vec<u8>> {
        self.inner
            .seek(SeekFrom::Start(0))
            .with_context(|| "failed to seek idx to start for read_all")?;
        let mut idx_vec = Vec::new();
        self.inner
            .read_to_end(&mut idx_vec)
            .with_context(|| "failed to read idx bytes in read_all")?;
        Ok(idx_vec)
    }

    pub fn close(mut self) -> Result<()> {
        let _ = self.inner.flush();
        Ok(())
    }
}

// IdxBlobWriter composes BlobWriter + IdxWriter to expose write_slice
pub struct IdxBlobWriter<W: Write + Read + Seek, I: Write + Read + Seek> {
    blob: BlobWriter<W>,
    idx: IdxWriter<I>,
}

impl<W: Write + Read + Seek, I: Write + Read + Seek> IdxBlobWriter<W, I> {
    pub fn new(blob_inner: W, idx_inner: I) -> Self {
        Self {
            blob: BlobWriter::new(blob_inner),
            idx: IdxWriter::new(idx_inner),
        }
    }

    /// Write slice to blob, append idx record, return the blob offset
    pub fn write_slice(&mut self, data: &[u8], ts: SystemTime) -> Result<u64> {
        let offset = self.blob.write_blob(data)?;
        let ts_millis = system_time_to_millis(ts)?;
        self.idx.write_idx(ts_millis, offset)?;
        Ok(offset)
    }

    /// Read all blob and idx bytes as a single operation
    pub fn read_all(&mut self) -> Result<(Vec<u8>, Vec<u8>)> {
        let blob_vec = self.blob.read_all()?;
        let idx_vec = self.idx.read_all()?;
        Ok((blob_vec, idx_vec))
    }

    /// Close both writers
    pub fn close(self) -> Result<()> {
        let _ = self.blob.close();
        let _ = self.idx.close();
        Ok(())
    }
}

// ---------- File-based rotate writer: IdxRotateFileWriter (previous behavior) ----------
struct CurrentFile {
    blob_path: PathBuf,
    idx_path: PathBuf,
    blob: File,
    idx: File,
    slice_count: usize,
}

impl CurrentFile {
    fn new(blob: File, idx: File, blob_path: PathBuf, idx_path: PathBuf) -> Self {
        Self {
            blob_path,
            idx_path,
            blob,
            idx,
            slice_count: 0,
        }
    }

    fn blob_write(&mut self, data: &[u8]) -> Result<u64> {
        let offset = self
            .blob
            .seek(SeekFrom::End(0))
            .with_context(|| format!("failed to seek blob {:?}", self.blob_path))?;
        self.blob
            .write_all(data)
            .with_context(|| format!("failed to write data to blob {:?}", self.blob_path))?;
        self.blob
            .flush()
            .with_context(|| format!("failed to flush blob {:?}", self.blob_path))?;
        Ok(offset)
    }

    fn idx_write(&mut self, ts_millis: u64, offset: u64) -> Result<()> {
        let line = IdxLine::new(ts_millis, offset);
        let mut buf = [0u8; 16];
        line.to_bytes(&mut buf)?;
        self.idx
            .write_all(&buf)
            .with_context(|| format!("failed to write idx to {:?}", self.idx_path))?;
        self.idx
            .flush()
            .with_context(|| format!("failed to flush idx {:?}", self.idx_path))?;
        Ok(())
    }

    fn slice_count(&self) -> usize {
        self.slice_count
    }

    fn inc_slice_count(&mut self) {
        self.slice_count += 1;
    }
}

/// File-backed writer that manages blob/idx files and rotation.
pub struct IdxRotateFileWriter {
    base_dir: PathBuf,
    id: String,
    max_slices_per_blob: usize,
    current: Option<CurrentFile>,
}

impl IdxRotateFileWriter {
    pub fn new(
        base_dir: impl Into<PathBuf>,
        id: impl Into<String>,
        max_slices_per_blob: usize,
    ) -> Self {
        Self {
            base_dir: base_dir.into(),
            id: id.into(),
            max_slices_per_blob,
            current: None,
        }
    }

    /// write a slice; auto-rotates on first write or when slice_count >= max_slices_per_blob
    pub fn write_slice(&mut self, data: &[u8], ts: impl Into<SystemTime>) -> Result<()> {
        let ts = ts.into();
        create_dir_all(&self.base_dir)
            .with_context(|| format!("failed to create directory {}", self.base_dir.display()))?;

        if self.current.is_none()
            || self
                .current
                .as_ref()
                .map(|c| c.slice_count() >= self.max_slices_per_blob)
                .unwrap_or(true)
        {
            self.rotate(ts)?;
        }

        let offset = {
            let cf = self
                .current
                .as_mut()
                .ok_or_else(|| anyhow!("blob not available after rotate"))?;
            cf.blob_write(data)?
        };

        let ts_millis = system_time_to_millis(ts)?;
        {
            let cf = self
                .current
                .as_mut()
                .ok_or_else(|| anyhow!("idx not available after rotate"))?;
            cf.idx_write(ts_millis, offset)?;
            cf.inc_slice_count();
        }
        Ok(())
    }

    /// rotate to a new file pair using provided timestamp
    pub fn rotate(&mut self, ts: SystemTime) -> Result<()> {
        let time_str = system_time_to_yyyymmddhhmmss(ts)?;
        let blob_name = format!("{}_{}.mp4", self.id, time_str);
        let idx_name = format!("{}.idx", blob_name);

        let blob_path = self.base_dir.join(&blob_name);
        let idx_path = self.base_dir.join(&idx_name);

        let blob_file = OpenOptions::new()
            .create(true)
            .append(true)
            .read(true)
            .open(&blob_path)
            .with_context(|| format!("failed to open blob file {:?}", blob_path))?;
        let idx_file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&idx_path)
            .with_context(|| format!("failed to open idx file {:?}", idx_path))?;

        self.current = Some(CurrentFile::new(blob_file, idx_file, blob_path, idx_path));
        Ok(())
    }

    /// list files for this writer id (same as previous)
    pub fn export_file_list(&self) -> Result<Vec<PathBuf>> {
        let mut out = Vec::new();
        let rd = read_dir(&self.base_dir).map_err(|e| {
            anyhow!(
                "failed to read directory {}: {}",
                self.base_dir.display(),
                e
            )
        })?;
        for entry in rd {
            let e = entry.map_err(|e| {
                anyhow!("failed to read entry in {}: {}", self.base_dir.display(), e)
            })?;
            let p = e.path();
            if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
                if name.starts_with(&format!("{}_", self.id))
                    && (name.ends_with(".mp4") || name.ends_with(".mp4.idx"))
                {
                    out.push(p);
                }
            }
        }
        Ok(out)
    }

    pub fn close(&mut self) -> Result<()> {
        self.current = None;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;
    use std::time::{Duration, UNIX_EPOCH};

    #[test]
    fn idxblobwriter_cursor_roundtrip() {
        let blob_cursor = Cursor::new(Vec::new());
        let idx_cursor = Cursor::new(Vec::new());
        let mut w = IdxBlobWriter::new(blob_cursor, idx_cursor);

        let st1 = UNIX_EPOCH + Duration::from_secs(1_700_000_000u64);
        let st2 = st1 + Duration::from_secs(60);

        let data1 = b"foo";
        let data2 = b"barbaz";
        w.write_slice(data1.as_ref(), st1).expect("write1");
        w.write_slice(data2.as_ref(), st2).expect("write2");

        let (blob_vec, idx_vec) = w.read_all().expect("read_all");

        assert_eq!(
            blob_vec.as_slice(),
            [data1.as_ref(), data2.as_ref()].concat().as_slice()
        );

        assert_eq!(idx_vec.len(), 16 * 2);

        let mut rec = [0u8; 16];
        rec.copy_from_slice(&idx_vec[0..16]);
        let line1 = IdxLine::parse_bytes(&rec).expect("parse idx1");
        let expected_ts1 = system_time_to_millis(st1).expect("ts1 millis");
        assert_eq!(line1.ts_millis(), expected_ts1);
        assert_eq!(line1.offset(), 0);

        rec.copy_from_slice(&idx_vec[16..32]);
        let line2 = IdxLine::parse_bytes(&rec).expect("parse idx2");
        let expected_ts2 = system_time_to_millis(st2).expect("ts2 millis");
        assert_eq!(line2.ts_millis(), expected_ts2);
        assert_eq!(line2.offset(), data1.len() as u64);
    }

    #[test]
    fn idx_writer_write_and_parse() {
        let idx_cursor = Cursor::new(Vec::new());
        let mut w = IdxWriter::new(idx_cursor);

        let ts1 = 1_700_000_000_000u64;
        let off1 = 0u64;
        let ts2 = 1_700_000_000_500u64;
        let off2 = 3u64;

        w.write_idx(ts1, off1).expect("write idx1");
        w.write_idx(ts2, off2).expect("write idx2");

        let idx_vec = w.read_all().expect("read_all");

        assert_eq!(idx_vec.len(), 16 * 2);

        let mut rec = [0u8; 16];
        rec.copy_from_slice(&idx_vec[0..16]);
        let l1 = IdxLine::parse_bytes(&rec).expect("parse1");
        assert_eq!(l1.ts_millis(), ts1);
        assert_eq!(l1.offset(), off1);

        rec.copy_from_slice(&idx_vec[16..32]);
        let l2 = IdxLine::parse_bytes(&rec).expect("parse2");
        assert_eq!(l2.ts_millis(), ts2);
        assert_eq!(l2.offset(), off2);
    }

    #[test]
    fn blob_writer_append_and_offsets() {
        let blob_cursor = Cursor::new(Vec::new());
        let mut b = BlobWriter::new(blob_cursor);

        let a = b"hello";
        let bdata = b"world!";
        let off_a = b.write_blob(a).expect("write a");
        assert_eq!(off_a, 0);
        let off_b = b.write_blob(bdata).expect("write b");
        assert_eq!(off_b, a.len() as u64);

        let blob_vec = b.read_all().expect("read_all");
        assert_eq!(
            blob_vec.as_slice(),
            [a.as_ref(), bdata.as_ref()].concat().as_slice()
        );
    }

    #[test]
    fn idxblobwriter_read_all_works() {
        let blob_cursor = Cursor::new(Vec::new());
        let idx_cursor = Cursor::new(Vec::new());
        let mut w = IdxBlobWriter::new(blob_cursor, idx_cursor);

        let st = UNIX_EPOCH + Duration::from_secs(1_700_000_000u64);
        w.write_slice(b"one", st).expect("write one");
        w.write_slice(b"two", st + Duration::from_secs(1))
            .expect("write two");

        let (blob_vec, idx_vec) = w.read_all().expect("read_all");

        assert_eq!(blob_vec, b"onetwo");
        assert_eq!(idx_vec.len(), 16 * 2);
    }
}
