import os
import zipfile
import tempfile
import logging
from pathlib import Path
from fnmatch import fnmatch

logger = logging.getLogger("codedthemes")

# ── Shared exclusion constants (used by both zip_repo and SyncManager) ────────

EXCLUDE_DIRS = {
    # Version control
    ".git", ".svn", ".hg",
    # Dependencies
    "node_modules", "bower_components", "vendor", "venv", ".venv", "env",
    # Build outputs
    "build", "dist", "out", "target", ".output", "_build",
    # Framework caches
    ".next", ".nuxt", ".svelte-kit", ".angular", ".turbo",
    ".parcel-cache", ".cache", ".temp", ".tmp",
    # IDE / editor
    ".idea", ".vscode", ".vs",
    # Python
    "__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
    # Testing
    "coverage", ".nyc_output", "htmlcov",
    # Misc
    ".terraform", ".serverless",
}

EXCLUDE_EXTENSIONS = {
    # Images
    ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico", ".svg",
    # Fonts
    ".woff", ".woff2", ".ttf", ".eot", ".otf",
    # Video / audio
    ".mp4", ".webm", ".avi", ".mov", ".mp3", ".wav", ".ogg",
    # Archives
    ".zip", ".tar", ".gz", ".rar", ".7z",
    # Documents
    ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
    # Source maps
    ".map",
    # Compiled / binary
    ".exe", ".dll", ".so", ".dylib", ".o", ".pyc", ".pyo", ".class",
}

EXCLUDE_FILES = {
    "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
    "composer.lock", "Gemfile.lock", "poetry.lock",
    ".DS_Store", "Thumbs.db", "desktop.ini",
}

MAX_SINGLE_FILE_BYTES = 2 * 1024 * 1024  # 2 MB


# ── .gitignore parsing ────────────────────────────────────────────────────────

def _load_gitignore_patterns(repo_root):
    """
    Reads .gitignore at the repo root and returns a list of patterns.
    Supports basic glob patterns; does NOT implement full git-ignore spec
    (negation, nested .gitignore, etc.) but covers the common cases.
    """
    gitignore_path = os.path.join(repo_root, ".gitignore")
    patterns = []
    if not os.path.isfile(gitignore_path):
        return patterns
    try:
        with open(gitignore_path, "r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                # Ignore negation patterns (!) for simplicity
                if line.startswith("!"):
                    continue
                patterns.append(line.rstrip("/"))
    except Exception:
        pass
    return patterns


def _is_gitignored(rel_path, patterns):
    """
    Checks if a relative path matches any .gitignore pattern.
    """
    parts = rel_path.replace("\\", "/").split("/")
    for pattern in patterns:
        # Directory-level match: check each path component
        for part in parts:
            if fnmatch(part, pattern):
                return True
        # Full-path match
        if fnmatch(rel_path.replace("\\", "/"), pattern):
            return True
        if fnmatch(rel_path.replace("\\", "/"), f"**/{pattern}"):
            return True
    return False


# ── Core functions ────────────────────────────────────────────────────────────

def detect_repo_root(start_path=None):
    """
    Checks if the current directory contains ai.json or .git.
    Strictly restricted to the current path (no upward traversal) to avoid 
    incorrectly detecting parent repos.
    """
    if not start_path:
        start_path = os.getcwd()

    current = Path(start_path).resolve()

    if any((current / f).exists() for f in ["ai.json", ".git"]):
        return current

    raise Exception("No repository root found. Please run this command inside a Git repository or one initialized with 'codedthemes init'.")




def zip_repo(repo_root):
    """
    Creates a temporary ZIP archive of the repository with aggressive filtering:
      - Excludes common dependency / build / cache directories
      - Excludes binary, media, font, and archive files by extension
      - Excludes lock files
      - Skips files larger than 2 MB
      - Respects .gitignore patterns at the repo root
    """
    temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
    temp_zip.close()

    gitignore_patterns = _load_gitignore_patterns(str(repo_root))
    file_count = 0
    skipped_count = 0

    with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as z:
        for root, dirs, files in os.walk(repo_root):
            # Prune excluded directories (in-place)
            dirs[:] = [
                d for d in dirs
                if d not in EXCLUDE_DIRS
                and not d.startswith(".")  # skip all hidden dirs (e.g. .env, .husky)
                or d in {".github"}  # but keep .github
            ]

            for file in files:
                full_path = os.path.join(root, file)
                rel_path = os.path.relpath(full_path, repo_root).replace(os.sep, "/")

                # Skip by exact filename
                if file in EXCLUDE_FILES:
                    skipped_count += 1
                    continue

                # Skip by extension
                _, ext = os.path.splitext(file)
                if ext.lower() in EXCLUDE_EXTENSIONS:
                    skipped_count += 1
                    continue

                # Skip large files
                try:
                    if os.path.getsize(full_path) > MAX_SINGLE_FILE_BYTES:
                        logger.debug(f"Skipping large file: {rel_path}")
                        skipped_count += 1
                        continue
                except OSError:
                    continue

                # Skip .gitignore-matched paths
                if gitignore_patterns and _is_gitignored(rel_path, gitignore_patterns):
                    skipped_count += 1
                    continue

                z.write(full_path, rel_path)
                file_count += 1

    zip_size_mb = os.path.getsize(temp_zip.name) / (1024 * 1024)
    logger.info(f"Zip created: {file_count} files, {zip_size_mb:.1f} MB (skipped {skipped_count})")

    if zip_size_mb > 50:
        logger.warning(
            f"Zip file is {zip_size_mb:.1f} MB — upload may be slow or fail. "
            "Consider adding large assets to .gitignore."
        )

    return temp_zip.name
