use super::{mkfs::read_superblock, *};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MountOptions {
    pub readonly: bool,
    pub replay_journal: bool,
}

impl MountOptions {
    pub const fn read_write() -> Self {
        Self {
            readonly: false,
            replay_journal: true,
        }
    }

    pub const fn read_only_no_journal_replay() -> Self {
        Self {
            readonly: true,
            replay_journal: false,
        }
    }
}

impl Ext4FileSystem {
    pub fn device_has_error_state<B: BlockDevice>(block_dev: &mut Jbd2Dev<B>) -> Ext4Result<bool> {
        let superblock = read_superblock(block_dev).map_err(|_| Ext4Error::io())?;
        if superblock.s_magic != EXT4_SUPER_MAGIC {
            return Err(Ext4Error::invalid_magic());
        }
        superblock.verify_superblock()?;
        Ok(superblock.s_state & Ext4Superblock::EXT4_ERROR_FS != 0)
    }

    /// Creates the root directory tree during bootstrap.
    fn create_root_dir<B: BlockDevice>(&mut self, block_dev: &mut Jbd2Dev<B>) -> Ext4Result<()> {
        // The actual on-disk initialization lives in the dedicated directory
        // bootstrap helper.
        create_root_directory_entry(self, block_dev)
    }

    fn dirty_for_mount(superblock: &mut Ext4Superblock) {
        superblock.s_state &= !Ext4Superblock::EXT4_VALID_FS;
        superblock.s_mnt_count = superblock.s_mnt_count.saturating_add(1);
    }

    fn inode_cache_size(superblock: &Ext4Superblock) -> usize {
        match superblock.s_inode_size {
            0 => DEFAULT_INODE_SIZE as usize,
            n => n as usize,
        }
    }

    fn reset_runtime_from_superblock<B: BlockDevice>(
        &mut self,
        block_dev: &mut Jbd2Dev<B>,
    ) -> Ext4Result<()> {
        self.group_count = self.superblock.block_groups_count();
        self.group_descs =
            Self::load_group_descriptors(block_dev, &self.superblock, self.group_count)?;
        self.block_allocator = BlockAllocator::new(&self.superblock);
        self.inode_allocator = InodeAllocator::new(&self.superblock);
        self.bitmap_cache = BitmapCache::create_default();
        self.inodetable_cache =
            InodeCache::new(INODE_CACHE_MAX, Self::inode_cache_size(&self.superblock));
        self.datablock_cache = DataBlockCache::new(DATABLOCK_CACHE_MAX, BLOCK_SIZE);
        Ok(())
    }

    fn reload_after_journal_replay<B: BlockDevice>(
        &mut self,
        block_dev: &mut Jbd2Dev<B>,
    ) -> Ext4Result<()> {
        self.superblock = read_superblock(block_dev).map_err(|_| Ext4Error::io())?;
        self.superblock.verify_superblock()?;
        Self::dirty_for_mount(&mut self.superblock);
        self.reset_runtime_from_superblock(block_dev)
    }

    fn clear_recovery_state(&mut self) {
        self.superblock.s_feature_incompat &= !Ext4Superblock::EXT4_FEATURE_INCOMPAT_RECOVER;
    }

    fn set_recovery_state(&mut self) {
        self.superblock.s_feature_incompat |= Ext4Superblock::EXT4_FEATURE_INCOMPAT_RECOVER;
    }

    fn valid_lost_found_hint<B: BlockDevice>(
        &mut self,
        block_dev: &mut Jbd2Dev<B>,
    ) -> Ext4Result<bool> {
        let ino = self.superblock.s_lpf_ino;
        if ino == 0 {
            return Ok(false);
        }

        let inode = self.get_inode_by_num(block_dev, InodeNumber::new(ino)?)?;
        Ok(inode.i_mode != 0 && inode.is_dir())
    }

    fn journal_blocks<B: BlockDevice>(
        &mut self,
        block_dev: &mut Jbd2Dev<B>,
        journal_inode: &mut Ext4Inode,
    ) -> Ext4Result<Vec<AbsoluteBN>> {
        let journal_block_count = journal_inode.size().div_ceil(BLOCK_SIZE as u64);
        let journal_block_map = resolve_inode_block_allextend(self, block_dev, journal_inode)?;
        let mut journal_blocks = Vec::new();
        for logical in 0..journal_block_count {
            let logical = u32::try_from(logical).map_err(|_| Ext4Error::corrupted())?;
            let phys = journal_block_map
                .get(&logical)
                .copied()
                .ok_or_else(Ext4Error::corrupted)?;
            journal_blocks.push(phys);
        }
        Ok(journal_blocks)
    }

    /// Mounts an ext4 filesystem from the given block device.
    pub fn mount<B: BlockDevice>(block_dev: &mut Jbd2Dev<B>) -> Result<Self, Ext4Error> {
        Self::mount_with_options(block_dev, MountOptions::read_write())
    }

    pub fn mount_with_options<B: BlockDevice>(
        block_dev: &mut Jbd2Dev<B>,
        options: MountOptions,
    ) -> Result<Self, Ext4Error> {
        debug!("Start mounting Ext4 filesystem...");

        // Mount flow:
        // 1. read and verify the superblock,
        // 2. load only enough metadata to locate/replay the journal,
        // 3. reload metadata from the recovered home blocks,
        // 4. repair bootstrap directories if they are missing.
        let mut superblock = read_superblock(block_dev).map_err(|_| Ext4Error::io())?;

        if superblock.s_magic != EXT4_SUPER_MAGIC {
            error!(
                "Invalid magic: {:#x}, expected: {:#x}",
                superblock.s_magic, EXT4_SUPER_MAGIC
            );
            return Err(Ext4Error::invalid_magic());
        }
        debug!("Superblock magic verified");
        superblock.verify_superblock()?;

        // Continue mounting even for an error-state filesystem so higher layers
        // can inspect or attempt repair.
        if superblock.s_state & Ext4Superblock::EXT4_ERROR_FS != 0 {
            warn!("Filesystem is in error state");
        }

        if !options.readonly {
            // Mark the filesystem as "not cleanly unmounted" before any writes.
            Self::dirty_for_mount(&mut superblock);
        }

        let group_count = superblock.block_groups_count();
        debug!("Block group count: {group_count}");

        let group_descs = Self::load_group_descriptors(block_dev, &superblock, group_count)?;
        debug!("Loaded {} group descriptors", group_descs.len());

        let block_allocator = BlockAllocator::new(&superblock);
        let inode_allocator = InodeAllocator::new(&superblock);
        debug!("Allocators initialized");

        let bitmap_cache = BitmapCache::create_default();
        debug!("Bitmap cache initialized (lazy loading)");

        // NOTE: inode size is a filesystem property (superblock.s_inode_size), not a fixed constant.
        // Using a wrong inode size will make inode table offsets incorrect and may read zeroed inodes
        // (e.g. /dev becomes mode=0, then VFS mount fails with ENOTDIR).
        let inode_cache = InodeCache::new(INODE_CACHE_MAX, Self::inode_cache_size(&superblock));
        debug!("Inode cache initialized");

        let datablock_cache = DataBlockCache::new(DATABLOCK_CACHE_MAX, BLOCK_SIZE);
        debug!("Data block cache initialized");

        let mut fs = Self {
            superblock,
            group_descs,
            block_allocator,
            inode_allocator,
            bitmap_cache,
            root_inode: InodeNumber::new(2)?,
            inodetable_cache: inode_cache,
            datablock_cache,
            group_count,
            mounted: true,
            journal_sb_block_start: None,
        };
        // Dump the core topology once so later failures have useful context in
        // the logs.
        debug_super_and_desc(&fs.superblock, &fs);

        // Journal bootstrap has two stages: ensure the journal inode exists,
        // then load its superblock and enable replay on the device wrapper.
        {
            let needs_recovery = fs
                .superblock
                .has_feature_incompat(Ext4Superblock::EXT4_FEATURE_INCOMPAT_RECOVER);

            if fs.superblock.has_journal() {
                let journal_inode_num = InodeNumber::new(JOURNAL_FILE_INODE as u32)?;
                let journal_inode = fs
                    .get_inode_by_num(block_dev, journal_inode_num)
                    .inspect_err(|e| {
                        error!("Failed to load journal inode {journal_inode_num}: {e}");
                    })?;
                let journal_exists = journal_inode.i_mode != 0;

                if fs
                    .superblock
                    .has_feature_compat(Ext4Superblock::EXT4_FEATURE_COMPAT_HAS_JOURNAL)
                    && !journal_exists
                {
                    if needs_recovery {
                        error!("Journal inode missing while filesystem needs recovery");
                        return Err(Ext4Error::corrupted());
                    }
                    create_journal_entry(&mut fs, block_dev).expect("create journal entry failed");
                }
            }
            if needs_recovery && options.replay_journal && !fs.superblock.has_journal() {
                error!("Filesystem needs journal recovery, but no journal is present");
                return Err(Ext4Error::corrupted());
            }
            if (block_dev.is_use_journal() || (needs_recovery && options.replay_journal))
                && fs.superblock.has_journal()
            {
                // By this point the journal inode must exist, so resolve its
                // first data block and hand the loaded journal superblock to
                // `Jbd2Dev`.
                let mut j_inode = fs
                    .get_inode_by_num(block_dev, InodeNumber::new(JOURNAL_FILE_INODE as u32)?)
                    .expect("load journal inode failed");

                let journal_blocks =
                    fs.journal_blocks(block_dev, &mut j_inode)
                        .inspect_err(|e| {
                            error!("Failed to resolve journal blocks: {e}");
                        })?;
                let journal_first_block = journal_blocks.first().copied().ok_or_else(|| {
                    error!("Journal has no mapped blocks");
                    Ext4Error::corrupted()
                })?;

                fs.journal_sb_block_start = Some(journal_first_block);
                let journal_data = fs
                    .datablock_cache
                    .get_or_load(block_dev, journal_first_block)
                    .expect("load journal superblock block failed")
                    .data
                    .clone();

                let j_sb = JournalSuperBllockS::from_disk_bytes(&journal_data);

                block_dev.set_journal_superblock_with_mapping(j_sb, journal_blocks)?;

                if needs_recovery && options.replay_journal {
                    // Replay before touching ordinary filesystem metadata.
                    // Until this completes, home blocks may be stale. A clean
                    // filesystem with journaling enabled still needs JBD2
                    // state initialized for future metadata writes, but it
                    // must not force replay without the ext4 recovery bit.
                    let original_journal_use = block_dev.is_use_journal();
                    if !original_journal_use {
                        info!("Filesystem needs journal recovery; enabling replay for mount");
                        block_dev.set_journal_use(true);
                    }
                    let replay_status = block_dev.journal_replay_checked();
                    block_dev.set_journal_use(original_journal_use);
                    if replay_status != ReplayStatus::Complete {
                        error!("Journal replay did not complete: status={replay_status:?}");
                        return Err(Ext4Error::corrupted());
                    }

                    // Journal replay can update the superblock, group
                    // descriptors, bitmaps, inode table, and directory blocks.
                    // Drop all metadata read before replay and continue
                    // mounting from the recovered on-disk state.
                    fs.reload_after_journal_replay(block_dev)?;
                    fs.clear_recovery_state();
                } else if !options.readonly && block_dev.is_use_journal() {
                    fs.set_recovery_state();
                }
            }
            // If the filesystem was created without a journal (e.g. small images
            // where mkfs.ext4 omits it), disable journal_use so that metadata
            // writes bypass the journal path instead of hitting the
            // "system uninitialized" guard on every write.
            if !fs.superblock.has_journal() {
                block_dev.set_journal_use(false);
            }
        }

        // rootinode check !
        {
            let root_inode = fs.get_root(block_dev).map_err(|e| {
                error!("Failed to load root inode: {e}");
                Ext4Error::io()
            })?;
            if root_inode.i_mode == 0 || !root_inode.is_dir() {
                if options.readonly {
                    error!(
                        "Root inode is uninitialized or not a directory, and read-only mount \
                         cannot repair it"
                    );
                    return Err(Ext4Error::corrupted());
                }
                warn!(
                    "Root inode is uninitialized or not a directory, creating root and \
                     lost+found... i_mode: {}, is_dir: {}",
                    root_inode.i_mode,
                    root_inode.is_dir()
                );
                fs.create_root_dir(block_dev).map_err(|_| Ext4Error::io())?;
            }
        }

        // Verify the recovery directory after the root directory is known good.
        {
            if fs.valid_lost_found_hint(block_dev)? {
                let ino = fs.superblock.s_lpf_ino;
                info!("/lost+found exists (superblock hint inode={ino})");
            } else {
                if fs.superblock.s_lpf_ino != 0 {
                    let ino = fs.superblock.s_lpf_ino;
                    warn!("s_lpf_ino={ino} is not a valid directory, falling back to path scan");
                }

                match get_file_inode(&mut fs, block_dev, "/lost+found") {
                    Ok(Some((ino, inode))) if inode.is_dir() => {
                        fs.superblock.s_lpf_ino = ino.raw();
                        if !options.readonly {
                            fs.sync_superblock(block_dev)?;
                        }
                        info!("/lost+found exists (path resolution, repaired hint inode={ino})");
                    }
                    Ok(Some((_ino, _inode))) => {
                        error!("/lost+found exists but is not a directory");
                        return Err(Ext4Error::corrupted());
                    }
                    Ok(None) => {
                        if options.readonly {
                            warn!("/lost+found missing and read-only mount cannot create it");
                        } else {
                            info!("/lost+found not found by path scan;will create!");
                            if create_lost_found_directory(&mut fs, block_dev).is_err() {
                                warn!("/lost+found missing and create failed");
                            }
                        }
                    }
                    Err(err) => {
                        error!("Failed to resolve /lost+found: {err}");
                        return Err(err);
                    }
                }
            }
        }

        // Emit a one-shot bitmap usage summary and verify bitmap checksums on
        // group 0 when metadata checksums are enabled.
        {
            let g0 = match fs.group_descs.first() {
                Some(desc) => desc,
                None => return Err(Ext4Error::bad_superblock()),
            };
            let inode_bitmap_blk = g0.inode_bitmap();
            let data_bitmap_blk = g0.block_bitmap();
            let inode_cache_key = CacheKey::new_inode(BGIndex::new(0));
            let data_cache_key = CacheKey::new_block(BGIndex::new(0));

            let inode_bitmap_data = fs
                .bitmap_cache
                .get_or_load(
                    block_dev,
                    inode_cache_key,
                    AbsoluteBN::new(inode_bitmap_blk),
                )
                .expect("block read failed")
                .clone();
            let blockbitmap_data = fs
                .bitmap_cache
                .get_or_load(block_dev, data_cache_key, AbsoluteBN::new(data_bitmap_blk))
                .expect("block read failed");

            if ext4_superblock_has_metadata_csum(&fs.superblock) {
                if !g0.is_inode_bitmap_uninit() {
                    let stored_inode = g0.inode_bitmap_csum(&fs.superblock);
                    let computed_inode =
                        ext4_inode_bitmap_csum32(&fs.superblock, &inode_bitmap_data.data);
                    let expected_inode = computed_inode;
                    if !g0.inode_bitmap_csum_matches(&fs.superblock, expected_inode) {
                        error!(
                            "Inode bitmap checksum mismatch group=0 expected={expected_inode:#x} \
                             stored={stored_inode:#x} inode_bitmap_block={inode_bitmap_blk} \
                             inode_table_block={} flags={:#x}",
                            g0.inode_table(),
                            g0.bg_flags
                        );
                        return Err(Ext4Error::checksum());
                    }
                }

                if !g0.is_block_bitmap_uninit() {
                    let stored_block = g0.block_bitmap_csum(&fs.superblock);
                    let computed_block =
                        ext4_block_bitmap_csum32(&fs.superblock, &blockbitmap_data.data);
                    let expected_block = computed_block;
                    if !g0.block_bitmap_csum_matches(&fs.superblock, expected_block) {
                        error!(
                            "Block bitmap checksum mismatch group=0 expected={expected_block:#x} \
                             stored={stored_block:#x} block_bitmap_block={data_bitmap_blk} \
                             inode_table_block={} flags={:#x}",
                            g0.inode_table(),
                            g0.bg_flags
                        );
                        return Err(Ext4Error::checksum());
                    }
                }
            }

            let mut inode_count: u64 = 0;
            let mut datablock_count: u64 = 0;
            let inode_data_array = &inode_bitmap_data.data;
            let datablock_array = &blockbitmap_data.data;

            inode_data_array.iter().for_each(|&bit| {
                let mut tmp = bit;
                loop {
                    if tmp == 0 {
                        break;
                    }
                    if tmp & 0x1 == 0x1 {
                        inode_count += 1;
                    }
                    tmp >>= 1;
                }
            });

            datablock_array.iter().for_each(|&bit| {
                let mut tmp = bit;
                loop {
                    if tmp == 0 {
                        break;
                    }
                    if tmp & 0x1 == 0x1 {
                        datablock_count += 1;
                    }
                    tmp >>= 1;
                }
            });

            debug!(
                "Bitmap usage: inodes used = {inode_count}, data blocks used = {datablock_count}"
            );
        }

        info!("Ext4 filesystem mounted");
        info!("  - block size: {} bytes", fs.superblock.block_size());
        info!("  - total blocks: {}", fs.superblock.blocks_count());
        info!("  - free blocks: {}", fs.superblock.free_blocks_count());
        info!("  - total inodes: {}", fs.superblock.s_inodes_count);
        info!("  - free inodes: {}", fs.superblock.s_free_inodes_count);
        // Flush metadata once at the end of mount so any replay state changes
        // or bootstrap repairs are persisted before normal operation begins.
        // The superblock is written with EXT4_VALID_FS cleared so a later mount
        // can distinguish an unclean shutdown from a real EXT4_ERROR_FS state.
        if !options.readonly {
            fs.sync_filesystem(block_dev)?;
            block_dev.umount_commit();
        }

        Ok(fs)
    }

    /// Loads all block-group descriptors in on-disk order.
    fn load_group_descriptors<B: BlockDevice>(
        block_dev: &mut Jbd2Dev<B>,
        superblock: &Ext4Superblock,
        group_count: u32,
    ) -> Result<Vec<Ext4GroupDesc>, Ext4Error> {
        let mut group_descs = Vec::new();
        let gdt_base: u64 = BLOCK_SIZE as u64;

        // Cache the currently loaded GDT block to avoid rereading the same
        // block for neighboring descriptors.
        let mut current_block: Option<AbsoluteBN> = None;

        let desc_size = superblock.get_desc_size() as usize;

        debug!("Loading group descriptors: {group_count} groups, desc_size = {desc_size} bytes");
        for group_id in 0..group_count {
            let byte_offset = gdt_base + group_id as u64 * desc_size as u64;
            let block_size_u64 = BLOCK_SIZE as u64;
            let block_num = AbsoluteBN::new(byte_offset / block_size_u64);
            let in_block = (byte_offset % block_size_u64) as usize;

            if current_block != Some(block_num) {
                block_dev
                    .read_block(block_num)
                    .map_err(|_| Ext4Error::io())?;
                current_block = Some(block_num);
            }

            let buffer = block_dev.buffer();
            let end = in_block + desc_size;
            if end > buffer.len() {
                error!(
                    "GDT out of range: group_id={}, in_block={}, desc_size={}, buffer_len={}",
                    group_id,
                    in_block,
                    desc_size,
                    buffer.len()
                );
                return Err(Ext4Error::bad_superblock());
            }

            let desc = Ext4GroupDesc::from_disk_bytes(&buffer[in_block..end]);
            desc.verify_checksum(superblock, group_id)?;
            group_descs.push(desc);
        }

        debug!(
            "Successfully loaded {} group descriptors",
            group_descs.len()
        );
        Ok(group_descs)
    }
}

/// Thin compatibility wrapper around [`Ext4FileSystem::mount`].
pub fn mount<B: BlockDevice>(block_dev: &mut Jbd2Dev<B>) -> Ext4Result<Ext4FileSystem> {
    mount_with_options(block_dev, MountOptions::read_write())
}

pub fn mount_with_options<B: BlockDevice>(
    block_dev: &mut Jbd2Dev<B>,
    options: MountOptions,
) -> Ext4Result<Ext4FileSystem> {
    match Ext4FileSystem::mount_with_options(block_dev, options) {
        Ok(_fs) => {
            info!("Ext4 filesystem mounted");
            Ok(_fs)
        }
        Err(e) => {
            error!("Mount failed: {e}");
            Err(e)
        }
    }
}
