import logging
import mmap
import platform
import re
import subprocess
import tempfile
from pathlib import Path

from seagoat.cache import Cache
from seagoat.repository import Repository
from seagoat.result import Result
from seagoat.sources.chroma import MAXIMUM_VECTOR_DISTANCE
from seagoat.utils.file_reader import read_file_with_correct_encoding
from seagoat.utils.file_types import is_file_type_supported

KILOBYTE = 1024
MEGABYTE = KILOBYTE * 1024
MAX_MMAP_SIZE = 100
MAX_MMAP_SIZE_BYTES = MAX_MMAP_SIZE * MEGABYTE
MAX_FILE_SIZE = 200 * KILOBYTE


class RipGrepCache(str):
    def __init__(self, repository):
        cache = Cache("ripgrep", Path(repository.path), {})
        self.repository = repository
        self.file_path = cache.get_cache_folder() / "mmap"
        if platform.system() == "Windows":
            self.file_path = tempfile.mktemp()
        self.is_initialized = False
        self._data = ""

    def _iterate_files_to_cache(self):
        for file, _ in self.repository.top_files():
            yield file

    def _iterate_lines_to_cache(self):
        for file in self._iterate_files_to_cache():
            file_contents = read_file_with_correct_encoding(file.absolute_path)

            if len(file_contents) > MAX_FILE_SIZE:
                logging.warning("Warning: file %s is too large to cache", file.path)
                continue

            for line_number, line in enumerate(file_contents.splitlines(), start=1):
                yield file, line_number, line

    def _generate_cache_lines(self):
        for file, line_number, line in self._iterate_lines_to_cache():
            yield f"{file.path}:{line_number}:{line}\n"

    def _build_cache_file(self):
        total_estimated_cache_size = 0
        line_count = 0

        with open(self.file_path, "w", encoding="utf-8") as cache_file:
            for formattted_cache_line in self._generate_cache_lines():
                cache_file.write(formattted_cache_line)
                total_estimated_cache_size += len(formattted_cache_line)
                line_count += 1

                if total_estimated_cache_size > MAX_MMAP_SIZE_BYTES:
                    logging.warning(
                        "Warning: maximum estimated ripgrep cache size of %s megabytes exceeded",
                        MAX_MMAP_SIZE,
                    )

                    break

        logging.info(
            "Estimated ripgrep cache size: %.2f megabytes (%s bytes). Line count %s",
            total_estimated_cache_size / MEGABYTE,
            int(total_estimated_cache_size),
            line_count,
        )
        self.is_initialized = True

    def rebuild(self):
        self._build_cache_file()
        if platform.system() == "Windows":
            # Memory map does not work on Windows for some reason
            # Use a simple string as a fallback
            with open(self.file_path, "r", encoding="utf-8") as cache_file:
                self._data = cache_file.read()
        else:
            with open(self.file_path, "r+b") as cache_file:
                self._data = mmap.mmap(cache_file.fileno(), 0)

    # pylint: disable-next=unused-argument
    def encode(self, *args, **kwargs):
        return self._data

    def as_input(self):
        if platform.system() == "Windows":
            return self.encode()

        return self


def _fetch(query_text: str, path: str, limit: int, cache: RipGrepCache):
    query_text = re.sub(r"\s+", "|", query_text)
    files = {}

    cmd = [
        "rg",
        "--max-count",
        str(limit),
        "--ignore-case",
        query_text,
    ]

    try:
        rg_output = subprocess.check_output(
            cmd, encoding="utf-8", input=cache.as_input()
        )
    except subprocess.CalledProcessError as exception:
        rg_output = exception.output
    for line in rg_output.splitlines():
        relative_path, raw_line_number, _ = line.split(":", 2)
        line_number = int(raw_line_number)

        absolute_path = Path(path) / relative_path

        if not is_file_type_supported(relative_path):
            continue

        if relative_path not in files:
            files[relative_path] = Result(str(relative_path), absolute_path)

        # This is so that ripgrep results are on comparable levels with chroma results
        files[relative_path].add_line(line_number, MAXIMUM_VECTOR_DISTANCE * 0.8)

    return files.values()


def initialize(repository: Repository):
    path = repository.path
    memory_cache = RipGrepCache(repository)

    def cache_chunk(_):
        # Ripgrep does not use chunks
        pass

    def cache_repo():
        memory_cache.rebuild()

    def fetch(query_text: str, limit: int):
        if not memory_cache.is_initialized:
            repository.analyze_files()
            cache_repo()

        return _fetch(query_text, str(path), limit, memory_cache)

    return {
        "fetch": fetch,
        "cache_chunk": cache_chunk,
        "cache_repo": cache_repo,
    }
