import fnmatch
import hashlib
import os
import tarfile
import tempfile

from occystrap import constants
from occystrap.filters.base import ImageFilter
from occystrap.tarformat import select_tar_format_for_layer
from shakenfist_utilities import logs


LOG = logs.setup_console(__name__)


class ExcludeFilter(ImageFilter):
    """Excludes files matching glob patterns from image layers.

    This filter rewrites layer tarballs to remove files and directories
    that match any of the specified glob patterns. Since this changes the
    layer content, the SHA256 hash is recalculated and the layer name is
    updated to match.

    This is useful for stripping unwanted content like .git directories,
    __pycache__ folders, or other files before writing output.
    """

    def __init__(self, wrapped_output, patterns,
                 temp_dir=None, diff_id_map=None):
        """Initialize the exclude filter.

        Args:
            wrapped_output: The ImageOutput to pass filtered elements to.
            patterns: List of glob patterns to exclude. Each pattern is
                matched against the full path using fnmatch.
            temp_dir: Directory for temporary files (default:
                system temp directory).
            diff_id_map: Shared dict for cross-image diff_id tracking
                (default: None).
        """
        super().__init__(
            wrapped_output, temp_dir=temp_dir,
            diff_id_map=diff_id_map)
        self.patterns = patterns

    def _matches_exclusion(self, path):
        """Check if a path matches any exclusion pattern.

        Args:
            path: The file path to check.

        Returns:
            True if the path should be excluded, False otherwise.
        """
        for pattern in self.patterns:
            if fnmatch.fnmatch(path, pattern):
                return True
        return False

    def _filter_layer(self, layer_data):
        """Filter a layer tarball, excluding matching entries.

        Creates a new tarball with entries that don't match exclusion
        patterns, calculates the new SHA256 hash, and returns both.

        Uses USTAR format when possible (smaller output), falls back to
        PAX format when layer contents require it. See tarformat.py.

        Args:
            layer_data: File-like object containing the original layer.

        Returns:
            Tuple of (filtered_file_handle, new_sha256_hex)
        """
        # Determine optimal tar format, skipping excluded members
        tar_format = select_tar_format_for_layer(
            layer_data,
            skip_fn=lambda m: self._matches_exclusion(m.name)
        )

        excluded_count = 0

        with tempfile.NamedTemporaryFile(
                delete=False, dir=self.temp_dir) as filtered_tf:
            try:
                with tarfile.open(fileobj=filtered_tf, mode='w',
                                  format=tar_format) as filtered_tar:
                    layer_data.seek(0)
                    with tarfile.open(fileobj=layer_data, mode='r') as layer_tar:
                        for member in layer_tar:
                            if self._matches_exclusion(member.name):
                                excluded_count += 1
                                continue

                            if member.isfile():
                                fileobj = layer_tar.extractfile(member)
                                filtered_tar.addfile(member, fileobj)
                            else:
                                filtered_tar.addfile(member)

                if excluded_count > 0:
                    LOG.info('Excluded %d entries from layer' % excluded_count)

                filtered_tf.flush()
                filtered_tf.seek(0)
                h = hashlib.sha256()
                while True:
                    chunk = filtered_tf.read(8192)
                    if not chunk:
                        break
                    h.update(chunk)

                new_sha = h.hexdigest()

                filtered_tf.seek(0)
                return open(filtered_tf.name, 'rb'), new_sha

            except Exception:
                os.unlink(filtered_tf.name)
                raise

    def process_image_element(self, element):
        """Process an image element, filtering layer
        contents.

        Config files are buffered so diff_ids can be
        updated in finalize(). Layers have matching
        entries excluded and their names updated to
        reflect the new SHA256 hash.
        """
        if element.element_type == constants.CONFIG_FILE:
            self._buffer_config(element)
        elif (element.element_type
                == constants.IMAGE_LAYER
                and element.data is not None):
            LOG.debug(
                'Filtering layer %s' % element.name)
            filtered_data, new_name = \
                self._filter_layer(element.data)
            self._record_new_diff_id(
                new_name, element.layer_index,
                original_hex=element.name)

            try:
                self._wrapped.process_image_element(
                    constants.ImageElement(
                        element.element_type,
                        new_name,
                        filtered_data,
                        layer_index=(
                            element.layer_index)))
            finally:
                try:
                    filtered_data.close()
                    os.unlink(filtered_data.name)
                except Exception:
                    pass
        else:
            if (element.element_type
                    == constants.IMAGE_LAYER):
                self._skip_layer(element.layer_index)
            self._wrapped.process_image_element(
                element)
