use crate::durability::{DurabilityLevel, FsyncGuard};
use crate::error::{QuarantineError, Result};
use crate::format::{FileHeader, Record, HEADER_SIZE};
use crate::recovery::{self, RecoveryReport};
use crate::segment::{SegmentManager, DEFAULT_MAX_SEGMENT_SIZE};
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Seek, SeekFrom};
use std::path::Path;
use tracing::{debug, info, instrument, warn};

/// The main quarantine journal.
///
/// Provides append-only storage with crash recovery, fsync guarantees,
/// and log rotation via segments.
pub struct Journal {
    segment_mgr: SegmentManager,
    current_file: Option<FsyncGuard>,
    current_offset: u64,
    durability: DurabilityLevel,
    last_recovery_report: Option<RecoveryReport>,
}

impl Journal {
    /// Open or create a journal in the specified directory.
    #[instrument(skip_all, fields(data_dir = %data_dir.as_ref().display()))]
    pub fn open(data_dir: impl AsRef<Path>) -> Result<Self> {
        Self::open_with_max_segment_size(data_dir, DEFAULT_MAX_SEGMENT_SIZE)
    }

    /// Open with a custom max segment size (useful for tests).
    #[instrument(skip_all, fields(data_dir = %data_dir.as_ref().display(), max_segment_size))]
    pub fn open_with_max_segment_size(
        data_dir: impl AsRef<Path>,
        max_segment_size: u64,
    ) -> Result<Self> {
        let data_dir = data_dir.as_ref().to_path_buf();
        let segment_mgr = SegmentManager::open(&data_dir, max_segment_size)?;

        let mut journal = Self {
            segment_mgr,
            current_file: None,
            current_offset: 0,
            durability: DurabilityLevel::Immediate,
            last_recovery_report: None,
        };

        journal.recover()?;
        info!(offset = journal.current_offset, "Journal opened");
        Ok(journal)
    }

    /// Set durability level.
    pub fn with_durability(mut self, level: DurabilityLevel) -> Self {
        self.durability = level;
        self
    }

    /// Get the current write offset.
    pub fn current_offset(&self) -> u64 {
        self.current_offset
    }

    /// Get the recovery report from the last open/recover call.
    pub fn recovery_report(&self) -> Option<&RecoveryReport> {
        self.last_recovery_report.as_ref()
    }

    /// Append a record to the journal.
    ///
    /// Checks if rotation is needed before writing. Returns the global offset.
    #[instrument(skip(self, payload), fields(payload_len = payload.len()))]
    pub fn append(&mut self, payload: Vec<u8>) -> Result<u64> {
        if self.current_file.is_none() {
            self.ensure_current_segment()?;
        }

        // Check if rotation is needed
        if let Some(guard) = &self.current_file {
            let current_size =
                guard.file().metadata().map_err(|e| QuarantineError::io(guard.path(), e))?.len();
            if self.segment_mgr.needs_rotation(current_size) {
                self.rotate()?;
            }
        }

        let record = Record::new(payload);
        let mut buf = Vec::with_capacity(record.disk_size() as usize);
        record.write_to(&mut buf)?;

        let guard = self.current_file.as_mut().ok_or_else(|| {
            QuarantineError::IoGeneric(std::io::Error::other("Journal file not open"))
        })?;
        guard.write(&buf)?;

        // Update the cached segment size to reflect the write.
        // This ensures read() can use the cached size for bounds checking.
        let new_file_size =
            guard.file().metadata().map_err(|e| QuarantineError::io(guard.path(), e))?.len();
        self.segment_mgr.update_current_segment_size(new_file_size);

        let offset = self.current_offset;
        self.current_offset += record.disk_size();

        debug!(offset, disk_size = record.disk_size(), "Record appended");
        Ok(offset)
    }

    /// Read a record at the given global offset.
    ///
    /// Resolves the correct segment via binary search, then seeks within it.
    /// If no segment is found, rescans the directory for new segments created
    /// by a separate writer instance and retries once.
    #[instrument(skip(self))]
    pub fn read(&mut self, offset: u64) -> Result<Record> {
        // Try to resolve the segment, refreshing once if not found
        let segment_count = self.segment_mgr.segments().len();
        debug!(offset, segment_count, "Resolving segment for read");

        let segment_info = match self.segment_mgr.resolve_segment(offset) {
            Some(seg) => {
                debug!(
                    offset,
                    base_offset = seg.base_offset,
                    size = seg.size,
                    path = %seg.path.display(),
                    "Found segment for offset"
                );
                (seg.base_offset, seg.path.clone(), seg.size)
            }
            None => {
                debug!(offset, "No segment found, refreshing...");
                // Segment not found - rescan directory for new segments
                self.segment_mgr.refresh()?;
                match self.segment_mgr.resolve_segment(offset) {
                    Some(seg) => {
                        debug!(
                            offset,
                            base_offset = seg.base_offset,
                            size = seg.size,
                            "Found segment after refresh"
                        );
                        (seg.base_offset, seg.path.clone(), seg.size)
                    }
                    None => {
                        debug!(
                            offset,
                            segment_count = self.segment_mgr.segments().len(),
                            "Still no segment after refresh"
                        );
                        return Err(QuarantineError::IoGeneric(std::io::Error::new(
                            std::io::ErrorKind::UnexpectedEof,
                            format!("No segment contains offset {}", offset),
                        )));
                    }
                }
            }
        };

        let local_offset = offset - segment_info.0;
        debug!(
            offset,
            base_offset = segment_info.0,
            local_offset,
            segment_size = segment_info.2,
            "Seeking to local offset in segment"
        );

        // Check if local_offset is beyond the segment's known size
        if local_offset >= segment_info.2 {
            debug!(
                local_offset,
                segment_size = segment_info.2,
                "Local offset beyond segment size, treating as EOF"
            );
            return Err(QuarantineError::IoGeneric(std::io::Error::new(
                std::io::ErrorKind::UnexpectedEof,
                format!(
                    "Offset {} beyond segment size {} (local_offset={})",
                    offset, segment_info.2, local_offset
                ),
            )));
        }

        let mut file =
            File::open(&segment_info.1).map_err(|e| QuarantineError::io(&segment_info.1, e))?;
        file.seek(SeekFrom::Start(local_offset))
            .map_err(|e| QuarantineError::io(&segment_info.1, e))?;

        let mut reader = BufReader::new(file);
        Record::read_from(&mut reader)
    }

    /// Force sync any pending writes.
    #[instrument(skip(self))]
    pub fn force_sync(&mut self) -> Result<()> {
        if let Some(ref mut guard) = self.current_file {
            guard.force_sync()?;
        }
        Ok(())
    }

    /// Force refresh the segment list from disk.
    ///
    /// Call this when notified of new writes from another Journal instance.
    /// This rescans the WAL directory for new segment files and updates their
    /// sizes, ensuring the read journal can see data written by the write journal.
    #[instrument(skip(self))]
    pub fn refresh_segments(&mut self) -> Result<()> {
        self.segment_mgr.refresh()
    }

    /// Clean up old segments below the given minimum cursor.
    ///
    /// Returns the number of bytes freed.
    #[instrument(skip(self))]
    pub fn cleanup(&mut self, min_cursor: u64) -> Result<u64> {
        self.segment_mgr.cleanup(min_cursor)
    }

    /// Recover state from disk using full record scanning across all segments.
    #[instrument(skip(self))]
    fn recover(&mut self) -> Result<()> {
        let segments = self.segment_mgr.segments().to_vec();

        if segments.is_empty() {
            debug!("No existing WAL segments, starting fresh");
            return Ok(());
        }

        // Recover each segment in order; stop at first with issues
        let mut total_valid = 0u64;
        let mut final_offset = 0u64;
        let mut last_report = None;

        for (i, segment) in segments.iter().enumerate() {
            let file_len = std::fs::metadata(&segment.path)
                .map_err(|e| QuarantineError::io(&segment.path, e))?
                .len();

            if file_len == 0 {
                debug!(base_offset = segment.base_offset, "Empty segment file, skipping");
                continue;
            }

            let report = recovery::recover_file(&segment.path)?;

            total_valid += report.valid_records;
            // The final_offset from recover_file is a local offset within the segment.
            // Convert to global: segment.base_offset + local_offset
            final_offset = segment.base_offset + report.final_offset;

            if report.bytes_truncated > 0 {
                warn!(
                    segment_index = i,
                    base_offset = segment.base_offset,
                    truncated = report.bytes_truncated,
                    "Segment had corrupt data, truncated"
                );
            }

            last_report = Some(report);
        }

        self.current_offset = final_offset;

        if let Some(report) = &last_report {
            if report.bytes_truncated > 0 {
                warn!(total_valid, final_offset, "Recovery truncated corrupt data");
            }
        }

        info!(total_valid, final_offset, "Multi-segment recovery complete");
        self.last_recovery_report = last_report;

        Ok(())
    }

    /// Ensure there's a current segment open for writing.
    fn ensure_current_segment(&mut self) -> Result<()> {
        if self.segment_mgr.segments().is_empty() {
            // First ever segment
            self.segment_mgr.create_segment(0)?;
            self.current_offset = HEADER_SIZE as u64;
        }

        self.open_current_file()
    }

    /// Rotate to a new segment.
    #[instrument(skip(self))]
    fn rotate(&mut self) -> Result<()> {
        // Close current file
        if let Some(mut guard) = self.current_file.take() {
            guard.force_sync()?;
        }

        let new_base = self.current_offset;
        self.segment_mgr.create_segment(new_base)?;

        // The new segment starts with a header, so the actual write position
        // within the segment is at HEADER_SIZE. But the global offset stays
        // at current_offset (which already accounts for everything written so far).
        // We do NOT advance current_offset by HEADER_SIZE here because the
        // segment's base_offset = current_offset, and reads will use
        // local_offset = global_offset - base_offset = 0 + HEADER_SIZE for the first record.
        //
        // Actually, we need the first record in the new segment to have a global
        // offset that, when converted to local, lands after the header.
        // local_offset = global_offset - base_offset
        // For the first record: local_offset should be HEADER_SIZE.
        // So global_offset = base_offset + HEADER_SIZE = current_offset + HEADER_SIZE.
        self.current_offset = new_base + HEADER_SIZE as u64;

        self.open_current_file()?;
        info!(new_base, "Rotated to new segment");

        Ok(())
    }

    #[instrument(skip(self))]
    fn open_current_file(&mut self) -> Result<()> {
        let segment = self.segment_mgr.current_segment().ok_or_else(|| {
            QuarantineError::IoGeneric(std::io::Error::other("No segments available"))
        })?;

        let path = segment.path.clone();

        let file = OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .truncate(false)
            .open(&path)
            .map_err(|e| QuarantineError::io(&path, e))?;

        let mut guard = FsyncGuard::new(file, path.clone(), self.durability);
        guard.lock_exclusive()?;

        let len = guard.file().metadata().map_err(|e| QuarantineError::io(&path, e))?.len();

        if len == 0 {
            let header = FileHeader::new();
            let mut buf = Vec::with_capacity(HEADER_SIZE);
            header.write_to(&mut buf)?;
            guard.write(&buf)?;
            debug!("Wrote v2 header to new segment");
        }

        // Seek to end for appends
        guard.file_mut().seek(SeekFrom::End(0)).map_err(|e| QuarantineError::io(&path, e))?;

        self.current_file = Some(guard);
        Ok(())
    }
}
