#!/usr/bin/env python3
"""
Organize .bin firmware files into intuitive structure.

Architecture:
  ~/Разработки/Прошивки/
    <Brand>/                          ← Kia-Hyundai, Lada, Ford, …
      <Model>/                        ← Cerato, i30, Vesta, …
        <CalibrationID-or-ECU>/       ← GTD-B46CQS3-C000, GFDVA46CFS0-6000 …
          Stock/                      ← заводская оригиналка
          Tune/                       ← Тюненые прошивки разных калибровщиков
          Euro2/                      ← С пониженным экологическим классом
          ImmoOff/                    ← С отключённым иммобилайзером
          Other/                      ← Что не классифицировалось
        … (другая калибровка той же модели)

В файлах префикс = калибровщик: `Paulus_Startpower_v1.bin`, `Ledokol_<orig>.bin` etc.
Хеш SHA-короткий для дедупликации.

Calibration ID берём ИЗ САМОГО BIN'А (offset 0x3FE2F) — надёжнее путей.
"""
import os, re, shutil, hashlib, sys

BASE = os.path.expanduser("~/Разработки/Прошивки")
EXTRACTED = os.path.join(BASE, "_extracted")

# --- Calibrator detection (collection author + tune brand names) ---
CALIBRATORS = [
    # Authors (priority — appear in path top-dir)
    ("paulus",       "Paulus"),
    ("ledokol",      "Ledokol"),
    ("chelyaba",     "Chelyaba"),
    ("motormaster",  "MotorMaster"),
    ("rsw",          "RSW"),
    ("mmk",          "MMK"),
    ("bashkirov",    "Bashkirov"),
    ("belettskiy",   "Belettskiy"),
    ("chipsoft",     "Chipsoft"),
    ("dirr",         "DIRR"),
    ("iron",         "Iron"),
    ("oktja",        "Oktja"),
    ("terminator",   "Terminator"),
    ("auteams",      "AUTEAMS"),
    ("turcentr",     "TurCentr"),
    ("rm-chiptuning","RM-chiptuning"),
    ("rm_chiptuning","RM-chiptuning"),
    ("esso_armeev",  "EssoArmeev"),
    ("esso ",        "EssoArmeev"),
    ("armeev",       "EssoArmeev"),
    ("ayashiko",     "Ayashiko"),
    ("bc consulting","BCconsulting"),
    ("ecudata",      "ECUData"),
    ("vovan123",     "Vovan123"),
    ("ourbrands",    "Ourbrands"),
    ("eurotun",      "EUROTUN"),
    ("otherauthors", "OtherAuthors"),
    ("popularauthors","PopularAuthors"),
    ("gowtuning",    "GOWtuning"),
    ("sergei",       "Sergei"),
    # Tune brand names
    ("startpower",   "Startpower"),
    ("gbo4",         "Startpower-GBO"),
    ("ct-team",      "CTTeam"),
    ("cttteam",      "CTTeam"),
    ("gt-team",      "GTTeam"),
    ("gt_team",      "GTTeam"),
    ("team_motors",  "TeamMotors"),
    ("romanprw",     "RomanPRW"),
    ("fly143",       "Fly143"),
    ("chiptuner",    "Chiptuner"),
]

BRAND_RULES = [
    (r"(\bkia\b|\bhyundai\b|cerato|rio|solaris|i30|venga|pikanto|picanto|elantra|sportage|tucson|carens|sorento|santa|ceed|optima|forte|accent|veloster|sonata)", "Kia-Hyundai"),
    (r"(gtd[_\-]?b46|gcqb|gcam|gayd|gcqa|gedva|gfdva|gags|gagd|gamd|gata[_\-]?be|gatab|gcrbreee|gcrbree|gcrbre|hcrd|hcr|gyn[_\-]?a|grbr)", "Kia-Hyundai"),
    (r"(vesta|xray|x[-_]?ray|granta|kalina|priora|largus|niva|chevy[_\-]?niva)", "Lada"),
    (r"\blada\b", "Lada"),
    (r"(\buaz\b|patriot)", "UAZ"),
    (r"(\bford\b|focus|kuga|mondeo|fusion|fiesta)", "Ford"),
    (r"(\brenault\b|duster|logan|sandero|megane|fluence|kangoo|symbol|terrano)", "Renault"),
    (r"(\bnissan\b|almera|murano)", "Nissan"),
    (r"(chevrolet|daewoo|lacetti|aveo|cruze|cobalt|matiz|nexia|lanos|orlando|spark)", "Chevrolet-Daewoo"),
    (r"(mitsubishi|melco|asx|outlander|lancer|pajero)", "Mitsubishi"),
    (r"(toyota|lexus|prado|lc150|land[_\-]?cruiser|camry|corolla|rav4)", "Toyota-Lexus"),
    (r"\bhonda\b", "Honda"),
    (r"\bmazda\b", "Mazda"),
    (r"\bsubaru\b", "Subaru"),
    (r"(\bvag\b|\bvw\b|\baudi\b|\bskoda\b|\bseat\b|polo|passat|golf|tiguan|octavia)", "VAG"),
    (r"cummins", "Cummins"),
    (r"mercedes|benz", "Mercedes"),
    (r"(ssang[_\-]?yong|actyon)", "SsangYong"),
    (r"(\bzaz\b|chance)", "ZAZ"),
    # extra brands often hiding in _Unsorted (matched against path/cal-id/filename)
    (r"peugeot", "Peugeot"),
    (r"citroen", "Citroen"),
    (r"(fiat|abarth)", "Fiat"),
    (r"alfa", "Alfa-Romeo"),
    (r"lancia", "Lancia"),
    (r"(opel|vauxhall)", "Opel"),
    (r"(bmw|\bmini[_\- ])", "BMW"),
    (r"infiniti", "Infiniti"),
    (r"(land[_\- ]?rover|landrover)", "Land-Rover"),
    (r"jaguar", "Jaguar"),
    (r"volvo", "Volvo"),
    (r"porsche", "Porsche"),
    (r"\bsaab\b", "Saab"),
    (r"(^|[/_\- ])1860[a-z]", "Mitsubishi"),         # MELCO part prefix
    (r"(^|[/_\- ])(21\d{3}|i\d{3}[a-z])", "Lada"),   # ВАЗ engine / Январь codes
    (r"china", "China"),
]

MODEL_KEYS = {
    "Kia-Hyundai": [
        "Cerato", "Rio", "Solaris", "i30", "Venga", "Picanto", "Pikanto",
        "Elantra", "Sportage", "Tucson", "Carens", "Sorento", "Ceed",
        "SantaFe", "Santa-Fe", "Optima", "Forte", "Accent", "Veloster",
        "Sonata", "Kona", "ix35", "Genesis"
    ],
    "Lada": ["Vesta", "XRay", "XRAY", "X-Ray", "Granta", "Kalina", "Priora",
             "Largus", "Niva", "ChevyNiva"],
    "Renault": ["Duster", "Logan", "Sandero", "Megane", "Fluence", "Kangoo",
                "Symbol", "Terrano", "Captur"],
    "Chevrolet-Daewoo": ["Lacetti", "Aveo", "Cruze", "Cobalt", "Matiz",
                         "Nexia", "Lanos", "Orlando", "Spark", "Niva"],
    "Ford": ["Focus", "Kuga", "Mondeo", "Fusion", "Fiesta"],
    "Nissan": ["Almera", "Murano", "Note", "Qashqai", "X-Trail"],
    "Mitsubishi": ["ASX", "Outlander", "Lancer", "Pajero", "Colt"],
    "Toyota-Lexus": ["Camry", "Corolla", "Rav4", "Prado", "LC150",
                     "LandCruiser", "Yaris", "Avensis"],
}

def classify_brand(path_str):
    p = path_str.lower()
    for pat, brand in BRAND_RULES:
        if re.search(pat, p):
            return brand
    return "_Unsorted"

def classify_model(path_str, brand):
    parts = [p for p in path_str.split('/') if p and not p.startswith('.')]
    if brand in MODEL_KEYS:
        for part in parts:
            for m in MODEL_KEYS[brand]:
                if m.lower() in part.lower():
                    return re.sub(r"[^A-Za-z0-9_-]", "_", m)
    # ECU fallback
    for ecu in ["ME17.9.11", "ME17.9.13", "ME17.9.21", "ME17.9.7", "ME17.9.71",
                "M7.9.7", "M7.9.8", "M74", "M86", "EDC15", "EDC16", "EDC17",
                "SIM2K", "Sim2K", "Sirius", "Delphi"]:
        if ecu.replace(".","").lower() in path_str.replace(".","").lower():
            return ecu.replace(".","_")
    if len(parts) >= 2:
        return re.sub(r"[^A-Za-z0-9._-]", "_", parts[1])[:40] or "_unknown"
    return "_unknown"

def detect_calibrator(path_str):
    p = path_str.lower()
    matches = []
    for key, name in CALIBRATORS:
        idx = p.find(key)
        if idx >= 0:
            matches.append((idx, name))
    if matches:
        matches.sort()  # earliest position wins (collection authors usually at top)
        return matches[0][1]
    return "Unknown"

def detect_calibration_id(file_data, fallback_path):
    """Extract calibration ID from bin (offset 0x3FE2F has cal-id string) or path."""
    # Try to read from .bin (Dataset block tail has cal-id ASCII)
    if file_data and len(file_data) >= 0x40000:
        # Look for ASCII string in the cal-id region (~0x3FE2F..0x3FE45)
        for start in [0x3FE2F, 0x3FE27, 0x3FE30]:
            chunk = file_data[start:start+24]
            txt = chunk.decode('latin1', errors='replace')
            # Look for typical patterns: GTD-..., GCQB..., GAGD..., etc.
            m = re.search(r"[GH][A-Z0-9_\-]{5,18}", txt)
            if m:
                cal = m.group(0)
                # Clean up
                cal = re.sub(r"[^A-Za-z0-9\-]", "-", cal)
                return cal
    # Fallback: try to find cal-id pattern in path
    m = re.search(r"(?:^|[/\-_ ])([GH][A-Z0-9]{2,4}[\-_]?[A-Z]{0,3}\d{0,2}[\-_][A-Z0-9]{3,6})(?:[/\-_ .])", fallback_path.upper())
    if m:
        cal = m.group(1)
        return re.sub(r"[^A-Za-z0-9\-]", "-", cal)
    return None

def classify_category(path_str):
    p = path_str.lower()
    fn = os.path.basename(p)

    # IMMO-OFF
    if re.search(r"immo[_\- ]?off|noimmo|no[_\- ]?immo|immooff", p):
        return "ImmoOff"
    if re.search(r"(^|[/_\-])io[_\- .]", p):
        return "ImmoOff"

    # EURO2
    if re.search(r"(^|[/_\- .])e2($|[_\- .])", p):
        return "Euro2"
    if re.search(r"eu2|euro[_\- ]?2", p):
        return "Euro2"

    # CVN-prefix factory format
    if "cvn" in fn:
        if re.search(r"e2|euro", p):
            return "Euro2"
        return "Stock"

    # Tune markers
    if re.search(r"\btun\b|tuned|chip[_\- ]?tune|stage[_\- ]?[12]|startpower|stp_", p):
        return "Tune"
    if "gbo" in fn:
        return "Tune"

    # Stock/original
    if re.search(r"\bori\b|original|stock|factor|0rg", p):
        return "Stock"

    # Default to Stock if filename is just the cal-id (factory bin)
    fn_base = os.path.splitext(fn)[0]
    if re.match(r"^[GH][A-Z0-9_\-]+\d*$", fn_base.upper()):
        return "Stock"

    return "Other"

def file_hash(data, prefix=10):
    h = hashlib.sha256(data).hexdigest()
    return h[:prefix]

def sanitize(s):
    return re.sub(r"[<>:\"|?*/\\]", "_", s)

# === Clean old non-_xxx dirs ===
if os.path.exists(BASE):
    for d in os.listdir(BASE):
        if d.startswith('_') or d.startswith('.'):
            continue
        full = os.path.join(BASE, d)
        if os.path.isdir(full):
            shutil.rmtree(full)

# === Process all firmware files ===
print(f"Scanning {EXTRACTED}...")
total = 0
copied = 0
skipped_dup = 0
size_total = 0
brand_counts = {}

for root, dirs, files in os.walk(EXTRACTED):
    for fn in files:
        if not fn.lower().endswith(('.bin', '.bbf', '.hex', '.s19', '.ori')):
            continue
        full = os.path.join(root, fn)
        rel = os.path.relpath(full, EXTRACTED)
        try:
            size = os.path.getsize(full)
        except OSError:
            continue
        if size < 16384 or size > 8*1024*1024:
            continue

        total += 1

        # Read file once for hash + cal-id detection
        try:
            with open(full, 'rb') as f:
                data = f.read()
        except OSError:
            continue

        brand = classify_brand(rel)
        model = classify_model(rel, brand)
        calibrator = detect_calibrator(rel)
        cat = classify_category(rel)
        # Fall back to a clear sentinel (not the model name) so we never produce a
        # confusing Model/Model double-folder when the calibration ID can't be read.
        cal_id = detect_calibration_id(data, rel) or "_без-калибровки"

        # Sanitize all components
        brand_s = sanitize(brand)
        model_s = sanitize(model)
        cal_id_s = sanitize(cal_id)
        cat_s = sanitize(cat)

        target_dir = os.path.join(BASE, brand_s, model_s, cal_id_s, cat_s)
        os.makedirs(target_dir, exist_ok=True)

        h = file_hash(data)
        base_name = os.path.splitext(fn)[0]
        ext = os.path.splitext(fn)[1]

        # Prefix with calibrator (unless "Unknown")
        prefix = f"[{calibrator}] " if calibrator != "Unknown" else ""
        target_name = f"{prefix}{base_name}__{h}{ext}"
        target_name = sanitize(target_name)

        target = os.path.join(target_dir, target_name)
        if os.path.exists(target):
            skipped_dup += 1
            continue
        shutil.copy2(full, target)
        copied += 1
        size_total += size
        brand_counts[brand] = brand_counts.get(brand, 0) + 1

# === Generate README/INDEX ===
readme = os.path.join(BASE, "_README.md")
with open(readme, 'w') as f:
    f.write("# 📁 Коллекция прошивок ЭБУ\n\n")
    f.write(f"Скачано с avto1000.com, распаковано паролем `veer90`.\n\n")
    f.write(f"**Всего файлов:** {copied}  •  **Размер:** {size_total/(1024*1024):.1f} MB\n\n")
    f.write("## Структура папок\n\n")
    f.write("```\nBrand/Model/CalibrationID/Category/[Calibrator] filename__hash.bin\n```\n\n")
    f.write("- **Brand** — Kia-Hyundai, Lada, Ford, Renault, Mitsubishi и т.п.\n")
    f.write("- **Model** — Cerato, i30, Vesta, Focus, Duster и т.п.\n")
    f.write("- **CalibrationID** — точный код калибровки прошивки из bin'а (например `GTD-B46CQS3-C000`)\n")
    f.write("- **Category:**\n")
    f.write("  - `Stock/` — заводская оригинальная прошивка\n")
    f.write("  - `Tune/` — тюненая (увеличение мощности, доработки)\n")
    f.write("  - `Euro2/` — пониженный экологический класс\n")
    f.write("  - `ImmoOff/` — с отключённым иммобилайзером\n")
    f.write("  - `Other/` — что не классифицировалось\n")
    f.write("- **[Calibrator]** — кто делал калибровку: Paulus, Ledokol, Chelyaba, MotorMaster, RSW и т.п.\n\n")
    f.write("## Сводка по брендам\n\n")
    f.write("| Бренд | Файлов |\n")
    f.write("|---|---|\n")
    for brand, count in sorted(brand_counts.items(), key=lambda x: -x[1]):
        f.write(f"| {brand} | {count} |\n")
    f.write("\n## Где искать наш Cerato GTD-B46CQS3-C000\n\n")
    f.write("`Kia-Hyundai/Cerato/GTD-B46CQS3-C000/` (если есть в коллекции)\n\n")
    f.write("Родственные калибровки Cerato 2 (TD): `GTD-B46CQS0-C000`, `GTD-B46CQS1-C000`, `GTD-B46CFS0-6000`, `GTD-B46CFS1-6000`.\n")
    f.write("Родственная модель: `i30` (тот же мотор 1.6 G4FC, та же платформа, рычаг круиза совместим).\n\n")
    f.write("## Служебные папки\n\n")
    f.write("- `_downloads/` — оригинальные архивы (скачка идёт сюда)\n")
    f.write("- `_extracted/` — промежуточная распаковка (можно удалить после раскладки)\n")
    f.write("- `_archives/` — резервные копии важных архивов\n")
    f.write("- `_logs/` — логи скачивания и распаковки\n")

print(f"\n=== DONE ===")
print(f"Total .bin found:   {total}")
print(f"Copied (unique):    {copied}")
print(f"Skipped (dup hash): {skipped_dup}")
print(f"Bytes:              {size_total/(1024*1024):.1f} MB")
print(f"\nREADME: {readme}")
