#!/usr/bin/env python3
"""
OTOV7 · Sprint A · Extract 3D data from data_room archives
============================================================
Scanne /opt/oto/data_room/{Pxx}/ pour extraire toutes les données nécessaires
à la génération 3D auto:
- IFC BIM squelette (aec_generated)
- Plans PDF préliminaires
- Feasibility docs (typologies, superficies, prix, ambiance/matériaux)
- Coordonnées géo (site)
- Rendus existants (200+ dans archives)

Output: /opt/oto/3d/data/{Pxx}_extracted.json
        /opt/oto/3d/data/manifest.json (global)

Usage:
    python3 oto_3d_extract.py            # tous les projets
    python3 oto_3d_extract.py P01        # un seul projet

Author: OTOV7 Core · 2026-08-10
"""
import os
import sys
import json
import re
from pathlib import Path
from datetime import datetime

try:
    import ifcopenshell
    HAS_IFC = True
except ImportError:
    HAS_IFC = False

try:
    import fitz  # PyMuPDF
    HAS_PDF = True
except ImportError:
    HAS_PDF = False

DATA_ROOM = Path("/opt/oto/data_room")
OUTPUT_DIR = Path("/opt/oto/3d/data")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# Metadata connue par projet (à enrichir avec les vraies faisabilités)
PROJECT_META = {
    "P01": {
        "name": "Coralis",
        "developer": "HRD",
        "location": {"city": "Santo Domingo", "sector": "Bella Vista", "country": "RD"},
        "clientele": "Jeunes cadres urbains 30-45",
        "palette": ["#0A0A0E", "#C9A465", "#FAF8F4", "#8B7355"],
        "materials": ["marbre carrare", "chêne massif", "laiton brossé", "verre trempé"],
        "typologies_typical": ["1C", "2C", "3C", "Penthouse"],
        "surface_m2": {"1C": 55, "2C": 85, "3C": 120, "Penthouse": 180},
    },
    "P02": {
        "name": "Coral del Sur",
        "developer": "Ristitullo",
        "location": {"city": "Santo Domingo", "sector": "Naco", "country": "RD"},
        "clientele": "Familles internationales",
        "palette": ["#F5F0EB", "#B8860B", "#4A5568", "#E6D5B8"],
        "materials": ["travertin", "bois exotique", "acier noir", "béton lissé"],
        "typologies_typical": ["Studio", "1C", "2C", "3C"],
        "surface_m2": {"Studio": 42, "1C": 60, "2C": 90, "3C": 130},
    },
    "P03": {
        "name": "Nakua",
        "developer": "HRD",
        "location": {"city": "Santo Domingo", "sector": "Bella Vista", "country": "RD"},
        "clientele": "Investisseurs internationaux",
        "palette": ["#1A1A2E", "#E94560", "#F5F5F5", "#0F3460"],
        "materials": ["granit noir", "acajou", "cuivre patiné", "verre fumé"],
        "typologies_typical": ["Studio", "1C", "2C", "3C", "Penthouse"],
        "surface_m2": {"Studio": 38, "1C": 58, "2C": 88, "3C": 128, "Penthouse": 220},
    },
    "P05": {
        "name": "Najayo",
        "developer": "HRD",
        "location": {"city": "San Cristóbal", "sector": "Najayo Arriba", "country": "RD"},
        "clientele": "Retraités + secondaire côtière",
        "palette": ["#FFFFFF", "#4A90E2", "#87CEEB", "#F5DEB3"],
        "materials": ["pierre naturelle", "bois clair", "aluminium blanc", "béton chaux"],
        "typologies_typical": ["1C", "2C", "3C"],
        "surface_m2": {"1C": 65, "2C": 95, "3C": 135},
    },
}


def find_ifc(project_code):
    """Trouve le fichier IFC principal du projet."""
    base = DATA_ROOM / project_code / "aec_generated"
    if not base.exists():
        return None
    ifcs = list(base.rglob("*_complete.ifc"))
    return ifcs[0] if ifcs else None


def parse_ifc(ifc_path):
    """Extract structure IFC (site, building, storeys, spaces)."""
    if not HAS_IFC or not ifc_path or not ifc_path.exists():
        return {"error": "IFC not available"}
    try:
        ifc = ifcopenshell.open(str(ifc_path))
        return {
            "file": str(ifc_path.relative_to(DATA_ROOM)),
            "size_bytes": ifc_path.stat().st_size,
            "schema": ifc.schema,
            "sites": [{"id": s.GlobalId, "name": s.Name} for s in ifc.by_type("IfcSite")],
            "buildings": [{"id": b.GlobalId, "name": b.Name} for b in ifc.by_type("IfcBuilding")],
            "storeys": [
                {"id": s.GlobalId, "name": s.Name, "elevation": getattr(s, "Elevation", None)}
                for s in ifc.by_type("IfcBuildingStorey")
            ],
            "spaces_count": len(ifc.by_type("IfcSpace")),
            "walls_count": len(ifc.by_type("IfcWall")),
            "doors_count": len(ifc.by_type("IfcDoor")),
            "windows_count": len(ifc.by_type("IfcWindow")),
        }
    except Exception as e:
        return {"error": str(e)}


def find_plans_pdfs(project_code):
    """Trouve les plans PDF (préliminaires + architectural)."""
    base = DATA_ROOM / project_code
    if not base.exists():
        return []
    plans = []
    patterns = ["*planos*.pdf", "*PLANOS*.pdf", "*plans*.pdf", "*architect*.pdf",
                "HRD_*.pdf", "*preliminaires*.pdf"]
    for pat in patterns:
        plans.extend([str(p.relative_to(DATA_ROOM)) for p in base.rglob(pat)])
    return sorted(set(plans))


def find_feasibility_docs(project_code):
    """Trouve les docs de faisabilité (JSON de préférence, sinon MD/PDF)."""
    base = DATA_ROOM / project_code
    if not base.exists():
        return {}
    docs = {
        "json_files": [str(p.relative_to(DATA_ROOM)) for p in base.rglob("*.json")][:30],
        "md_files": [str(p.relative_to(DATA_ROOM)) for p in base.rglob("*.md")][:20],
        "financier_pdfs": [str(p.relative_to(DATA_ROOM)) for p in (base / "50_financier_bancable").glob("*.pdf")] if (base / "50_financier_bancable").exists() else [],
    }
    return docs


def find_existing_renders(project_code):
    """Trouve rendus existants (PNG/JPG/WebP) dans archives."""
    base = DATA_ROOM / project_code
    if not base.exists():
        return []
    renders = []
    for ext in ["png", "jpg", "jpeg", "webp"]:
        renders.extend([str(p.relative_to(DATA_ROOM)) for p in base.rglob(f"*.{ext}")])
    return sorted(set(renders))[:100]


def extract_feasibility_units(project_code):
    """Parse les JSON de faisabilité pour trouver les données d'unités."""
    base = DATA_ROOM / project_code
    if not base.exists():
        return {}
    units_data = {}
    for jf in list(base.rglob("*unites*.json")) + list(base.rglob("*units*.json")):
        try:
            data = json.loads(jf.read_text(encoding="utf-8"))
            units_data[str(jf.relative_to(DATA_ROOM))] = data
        except Exception:
            pass
    for jf in list(base.rglob("*typolog*.json")):
        try:
            data = json.loads(jf.read_text(encoding="utf-8"))
            units_data.setdefault("typologies", {})[str(jf.relative_to(DATA_ROOM))] = data
        except Exception:
            pass
    return units_data


def extract_project(project_code):
    """Extract complet d'un projet."""
    print(f"\n=== Extract {project_code} ===")
    ifc_path = find_ifc(project_code)
    meta = PROJECT_META.get(project_code[:3], {})

    data = {
        "project_code": project_code,
        "meta": meta,
        "extracted_at": datetime.utcnow().isoformat() + "Z",
        "ifc": parse_ifc(ifc_path) if ifc_path else {"error": "no IFC found"},
        "plans_pdfs": find_plans_pdfs(project_code),
        "feasibility_docs": find_feasibility_docs(project_code),
        "existing_renders": find_existing_renders(project_code),
        "units_from_feasibility": extract_feasibility_units(project_code),
    }

    # Stats
    data["stats"] = {
        "ifc_available": bool(ifc_path),
        "ifc_storeys": len(data["ifc"].get("storeys", [])),
        "plans_pdf_count": len(data["plans_pdfs"]),
        "feasibility_json_count": len(data["feasibility_docs"].get("json_files", [])),
        "existing_renders_count": len(data["existing_renders"]),
    }

    print(f"  IFC: {'✓' if data['stats']['ifc_available'] else '✗'} · Storeys: {data['stats']['ifc_storeys']}")
    print(f"  Plans PDF: {data['stats']['plans_pdf_count']}")
    print(f"  Feasibility JSON: {data['stats']['feasibility_json_count']}")
    print(f"  Renders existants: {data['stats']['existing_renders_count']}")

    out_file = OUTPUT_DIR / f"{project_code}_extracted.json"
    out_file.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"  → {out_file}")
    return data


def build_manifest(all_extracts):
    """Manifest global pour le viewer frontend."""
    manifest = {
        "generated_at": datetime.utcnow().isoformat() + "Z",
        "projects": {},
    }
    for code, data in all_extracts.items():
        meta = data.get("meta", {})
        manifest["projects"][code] = {
            "name": meta.get("name", code),
            "location": meta.get("location", {}),
            "clientele": meta.get("clientele", ""),
            "storeys": data["stats"]["ifc_storeys"],
            "renders_available": data["stats"]["existing_renders_count"],
            "ifc_ready": data["stats"]["ifc_available"],
            "typologies": meta.get("typologies_typical", []),
            "extract_path": f"{code}_extracted.json",
        }
    (OUTPUT_DIR / "manifest.json").write_text(
        json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
    )
    print(f"\n=== Manifest global → {OUTPUT_DIR}/manifest.json ===")
    return manifest


if __name__ == "__main__":
    if len(sys.argv) > 1:
        project = sys.argv[1]
        projects = [project]
    else:
        # Auto-detect all P0X folders in data_room
        projects = sorted([p.name for p in DATA_ROOM.iterdir()
                           if p.is_dir() and re.match(r"^P\d{2}", p.name)])
        print(f"Projects trouvés: {projects}")

    all_extracts = {}
    for p in projects:
        try:
            all_extracts[p] = extract_project(p)
        except Exception as e:
            print(f"  ERROR {p}: {e}")

    build_manifest(all_extracts)
    print(f"\n✅ Sprint A terminé · {len(all_extracts)} projets extraits")
