import os
import subprocess
from pathlib import Path
import sys
import shutil
import time
import json
from remake_config import *

# ========== COLOR UTILS ==========
class Colors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    BOLD = '\033[1m'
    RESET = '\033[0m'
    GRAY = '\033[90m'

def color(text, style): return f"{style}{text}{Colors.RESET}"
def banner(title): print(color(f"\n╔═ {title} ═══════════════════════════════╗", Colors.BOLD + Colors.OKBLUE))
def info(msg): print(color(f"✅ {msg}", Colors.OKGREEN))
def warn(msg): print(color(f"⚠️  {msg}", Colors.WARNING))
def error(msg): print(color(f"❌ {msg}", Colors.FAIL))

def log(msg: str):
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(msg + "\n")

# ========== CACHE ==========
def load_cache():
    try:
        if CACHE_FILE.exists():
            return json.loads(CACHE_FILE.read_text())
    except json.decoder.JSONDecodeError:
        error("Failed to Read Cache File.")
    return {}

def save_cache(data):
    CACHE_FILE.write_text(json.dumps(data, indent=2))

# ========== PACKAGE DISCOVERY ==========
class AutoLib:
    def __init__(self, name):
        self.name = name
        self.path = None

    def find(self, search_paths, cache):
        if self.name in cache:
            self.path = Path(cache[self.name])
            return self.path.exists()

        for path in search_paths:
            for root, _, files in os.walk(path):
                for ext in [".lib", ".a"]:
                    fname = f"lib{self.name}{ext}"
                    if fname in files:
                        self.path = Path(root) / fname
                        cache[self.name] = str(self.path)
                        info(f"Found {self.name} at {self.path}")
                        return True
        return False

class AutoInclude:
    def __init__(self, name):
        self.name = name
        self.path = None

    def find(self, search_paths, cache):
        if self.name in cache:
            self.path = Path(cache[self.name])
            return self.path.exists()

        for path in search_paths:
            for root, _, files in os.walk(path):
                if f"{self.name}.h" in files or Path(root).name == self.name:
                    self.path = Path(root)
                    cache[self.name] = str(self.path)
                    info(f"Found header {self.name} at {self.path}")
                    return True
        return False

def resolve_packages():
    cache = load_cache()
    extra_link_paths, resolved_libs, extra_includes = [], [], []

    for name in AUTO_LIBS:
        lib = AutoLib(name)
        if lib.find(LIB_DIRS, cache):
            extra_link_paths.append(str(lib.path.parent))
            resolved_libs.append(f"-l{lib.name}")
        else:
            error(f"Library {lib.name} not found.")
            sys.exit(1)

    for name in AUTO_INCLUDES:
        inc = AutoInclude(name)
        if inc.find(INCLUDE_DIRS, cache):
            extra_includes.append(str(inc.path))

    save_cache(cache)
    return list(set(extra_link_paths)), resolved_libs, list(set(extra_includes))

# ========== BUILD SYSTEM ==========
def find_cpp_files():
    cpp_files = []
    for folder in SRC_DIRS:
        for path in Path(folder).rglob("*.cpp"):
            cpp_files.append(path)
    return cpp_files

def obj_path(source): return BUILD_DIR / source.relative_to("src").with_suffix(".o")
def dep_path(obj): return obj.with_suffix(".d")

def parse_dep_file(dep_file):
    if not dep_file.exists(): return []
    deps = []
    with dep_file.open() as f:
        for line in f:
            line = line.strip().replace("\\", "")
            if ":" in line:
                line = line.split(":", 1)[1]
            deps.extend(line.strip().split())
    return deps

def compile_source(source, obj, includes):
    obj.parent.mkdir(parents=True, exist_ok=True)
    cmd = [CXX, *CXXFLAGS, *[f"-I{inc}" for inc in includes], "-MMD", "-MP", "-c", str(source), "-o", str(obj)]
    try:
        print(f"{color('🔨 Compiling:', Colors.OKCYAN)} {source}")
        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        log(f"[COMPILE] {' '.join(cmd)}")
    except subprocess.CalledProcessError as e:
        error(f"Failed to compile {source}")
        print("🔧 Command:", " ".join(cmd))
        print(e.stderr.decode())
        log(f"[ERROR] {' '.join(cmd)}\n{e.stderr.decode()}")
        sys.exit(1)

def link_objects(obj_files, link_dirs, libs):
    cmd = [CXX, *map(str, obj_files), "-o", str(TARGET), *[f"-L{p}" for p in link_dirs], *libs]
    try:
        print(f"{color('📦 Linking:', Colors.OKBLUE)} {TARGET}")
        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        log(f"[LINK] {' '.join(cmd)}")
    except subprocess.CalledProcessError as e:
        error("Linking failed.")
        print("🔧 Command:", " ".join(cmd))
        print(e.stderr.decode())
        log(f"[ERROR] {' '.join(cmd)}\n{e.stderr.decode()}")
        sys.exit(1)

def build():
    build_start = time.time()
    banner("🚀 Building Project")
    cpp_files = find_cpp_files()
    obj_files = []

    link_dirs, libs, extra_includes = resolve_packages()
    all_includes = INCLUDE_DIRS + extra_includes

    for source in cpp_files:
        obj = obj_path(source)
        dep = dep_path(obj)
        obj_mtime = obj.stat().st_mtime if obj.exists() else 0
        needs_build = not obj.exists() or source.stat().st_mtime > obj_mtime

        if not needs_build and dep.exists():
            for dep_file in parse_dep_file(dep):
                try:
                    if Path(dep_file).exists() and Path(dep_file).stat().st_mtime > obj_mtime:
                        needs_build = True
                        break
                except Exception:
                    needs_build = True
                    break

        if needs_build:
            compile_source(source, obj, all_includes)
        else:
            print(f"{color('👌 Up-to-date:', Colors.GRAY)} {source}")
        obj_files.append(obj)

    link_objects(obj_files, link_dirs, libs)
    banner("✅ Build Complete")
    print(color(f"⏱ Build time: {time.time() - build_start:.2f}s", Colors.OKCYAN))

def run():
    build()
    if TARGET.exists():
        banner("🚀 Running")
        try:
            subprocess.run(str(TARGET), check=True)
            log("[RUN] Executed app.exe successfully.")
        except subprocess.CalledProcessError as e:
            error("Program exited with error.")
            log(f"[ERROR] Runtime crash\n{e}")
            sys.exit(e.returncode)
    else:
        error("Executable not found.")

def clean():
    banner("🧹 Cleaning")
    if BUILD_DIR.exists():
        shutil.rmtree(BUILD_DIR)
        info("Build directory removed.")
    if LOG_FILE.exists():
        LOG_FILE.unlink()
        info("Log file cleared.")
    if CACHE_FILE.exists():
        CACHE_FILE.unlink()
        info("Cache file cleared.")

# ========== ENTRY ==========
if __name__ == "__main__":
    start = time.time()
    LOG_FILE.write_text("", encoding="utf-8")

    try:
        if "clean" in sys.argv:
            clean()
        elif "run" in sys.argv:
            run()
        else:
            build()
    except KeyboardInterrupt:
        error("Interrupted by user.")
        log("[ERROR] Interrupted by user.")
        sys.exit(1)

    print(color(f"\n⏱ Total time: {time.time() - start:.2f}s", Colors.BOLD + Colors.OKGREEN))