#!/usr/bin/env python3
# ──────────────────────────────────────────────
# saqr (صقر) — file integrity watchdog
# lock your files. check if anyone touched them.
# by: @gqnxx
# ──────────────────────────────────────────────

from __future__ import annotations

import hashlib
import json
import os
import sys
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Set, Tuple

# ──────────────────────────────────────────────
# config
# ──────────────────────────────────────────────

LOCK_FILE = ".saqr.lock"
CHUNK_SIZE = 8192

SKIP_DIRS: Set[str] = {".git", "__pycache__", "node_modules", ".venv", "venv", ".next", "dist", "build"}
SKIP_FILES: Set[str] = {".DS_Store", "Thumbs.db"}

# ──────────────────────────────────────────────
# colors
# ──────────────────────────────────────────────

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
WHITE = "\033[97m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"

# ──────────────────────────────────────────────
# core
# ──────────────────────────────────────────────


def hash_file(filepath: str) -> Optional[str]:
    """sha256 of a file"""
    h = hashlib.sha256()
    try:
        with open(filepath, "rb") as f:
            while True:
                chunk = f.read(CHUNK_SIZE)
                if not chunk:
                    break
                h.update(chunk)
        return h.hexdigest()
    except (PermissionError, OSError):
        return None


def scan_directory(target_dir: str) -> Dict[str, Dict[str, Any]]:
    """walk every file and return {path: {hash, size}}"""
    fingerprints: Dict[str, Dict[str, Any]] = {}
    for root, dirs, files in os.walk(target_dir):
        filtered = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
        dirs.clear()
        dirs.extend(filtered)
        for fname in files:
            if fname == LOCK_FILE or fname in SKIP_FILES:
                continue
            filepath = os.path.join(root, fname)
            rel_path = os.path.relpath(filepath, target_dir)
            file_hash = hash_file(filepath)
            if file_hash is not None:
                fingerprints[rel_path] = {
                    "hash": file_hash,
                    "size": os.path.getsize(filepath),
                }
    return fingerprints


def time_ago(timestamp: str) -> str:
    """human readable time diff"""
    try:
        locked = datetime.fromisoformat(timestamp)
        diff = datetime.now() - locked
        if diff.days > 0:
            return f"{diff.days}d ago"
        hours = diff.seconds // 3600
        if hours > 0:
            return f"{hours}h ago"
        minutes = diff.seconds // 60
        if minutes > 0:
            return f"{minutes}m ago"
        return "just now"
    except (ValueError, TypeError):
        return "unknown"


# ──────────────────────────────────────────────
# commands
# ──────────────────────────────────────────────


def cmd_lock(target_dir: str) -> None:
    """lock — snapshot all file hashes"""
    target_dir = os.path.abspath(target_dir)

    if not os.path.isdir(target_dir):
        print(f"{RED}✗ not a directory: {target_dir}{RESET}")
        sys.exit(1)

    print(f"\n{GREEN}{BOLD}صقر{RESET} {DIM}— locking{RESET}")
    print(f"{DIM}{target_dir}{RESET}\n")

    fingerprints = scan_directory(target_dir)
    lock_data = {
        "locked_at": datetime.now().isoformat(),
        "target": target_dir,
        "file_count": len(fingerprints),
        "files": fingerprints,
    }

    lock_path = os.path.join(target_dir, LOCK_FILE)
    with open(lock_path, "w") as f:
        json.dump(lock_data, f, indent=2)

    print(f"{GREEN}{BOLD}🔒 locked {len(fingerprints)} files{RESET}\n")


def cmd_check(target_dir: str) -> None:
    """check — compare current state against lock"""
    target_dir = os.path.abspath(target_dir)
    lock_path = os.path.join(target_dir, LOCK_FILE)

    if not os.path.exists(lock_path):
        print(f"\n{RED}✗ no lock found — run 'lock' first{RESET}\n")
        sys.exit(1)

    with open(lock_path, "r") as f:
        lock_data = json.load(f)

    print(f"\n{GREEN}{BOLD}صقر{RESET} {DIM}— checking{RESET}")
    print(f"{DIM}locked {time_ago(lock_data['locked_at'])}{RESET}\n")

    old_files: Dict[str, Any] = lock_data["files"]
    new_files: Dict[str, Dict[str, Any]] = scan_directory(target_dir)

    modified: List[Tuple[str, int]] = []
    added: List[Tuple[str, int]] = []
    deleted: List[str] = []
    clean_files: List[str] = []

    for raw_path in list(old_files.keys()):
        file_path: str = str(raw_path)
        old_info: Dict[str, Any] = old_files[file_path]
        if file_path in new_files:
            if new_files[file_path]["hash"] != old_info["hash"]:
                old_size: int = int(old_info["size"])
                new_size: int = int(new_files[file_path]["size"])
                size_diff: int = new_size - old_size
                modified.append((file_path, size_diff))
            else:
                clean_files.append(file_path)
        else:
            deleted.append(file_path)

    for raw_new_path in list(new_files.keys()):
        new_path: str = str(raw_new_path)
        if new_path not in old_files:
            added.append((new_path, int(new_files[new_path]["size"])))

    # results

    for path, size_diff in modified:
        sign = f"+{size_diff}" if size_diff >= 0 else str(size_diff)
        print(f"  {RED}✗ modified{RESET}  {path}  {DIM}({sign} bytes){RESET}")

    for path, size in added:
        print(f"  {YELLOW}+ added{RESET}     {path}  {DIM}({size} bytes){RESET}")

    for path in deleted:
        print(f"  {RED}⚫ deleted{RESET}  {path}")

    if len(clean_files) > 0:
        print(f"  {GREEN}✓ {len(clean_files)} files unchanged{RESET}")

    total_changes = len(modified) + len(added) + len(deleted)

    print()
    if total_changes == 0:
        print(f"  {GREEN}{BOLD}all clear ✓{RESET}\n")
    else:
        print(f"  {RED}{BOLD}{total_changes} change(s) detected{RESET}\n")


def cmd_status(target_dir: str) -> None:
    """status — is this directory locked?"""
    target_dir = os.path.abspath(target_dir)
    lock_path = os.path.join(target_dir, LOCK_FILE)

    if not os.path.exists(lock_path):
        print(f"\n  {DIM}not locked{RESET}\n")
        return

    with open(lock_path, "r") as f:
        lock_data = json.load(f)

    print(f"\n{GREEN}{BOLD}صقر{RESET}")
    print(f"  🔒 locked: {GREEN}yes{RESET}")
    print(f"  📁 files:  {lock_data['file_count']}")
    print(f"  🕐 since:  {time_ago(lock_data['locked_at'])}")
    print()


# ──────────────────────────────────────────────
# usage
# ──────────────────────────────────────────────

USAGE = f"""
{GREEN}{BOLD}saqr{RESET} {DIM}(صقر) — file integrity watchdog{RESET}
{DIM}by @gqnxx{RESET}

{BOLD}usage:{RESET}
  saqr lock   <dir>   snapshot file hashes
  saqr check  <dir>   detect changes since lock
  saqr status <dir>   show lock info

{DIM}saqr caught something? buy me a coffee before the hacker does ☕{RESET}
{DIM}https://buymeacoffee.com/batdown{RESET}
"""


def main() -> None:
    if len(sys.argv) < 3:
        print(USAGE)
        sys.exit(0)

    command = sys.argv[1].lower()
    target = sys.argv[2]

    commands: Dict[str, Callable[[str], None]] = {
        "lock": cmd_lock,
        "check": cmd_check,
        "status": cmd_status,
    }

    if command in commands:
        commands[command](target)
    else:
        print(f"{RED}✗ unknown: {command}{RESET}")
        print(USAGE)
        sys.exit(1)


if __name__ == "__main__":
    main()
