#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FreeCAD Model Analyzer API - Parametrik Bounding Box ve Malzeme Ağırlık Hesaplayıcı
Web API ve CLI uyumlu analiz aracı 
freecadcmd free.py freecad1.stp cikti.png
"""

import FreeCAD
import FreeCAD as App
import sys
import os
import json
import argparse
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
from pathlib import Path


# ============================================================================
# KONFİGÜRASYON SINIFLARI
# ============================================================================

@dataclass
class MaterialConfig:
    """Malzeme özellikleri"""
    name: str = "Aluminium"
    density_kg_dm3: float = 2.71  # Alüminyum: 2.71 kg/dm³
    symbol: str = "Al"


@dataclass
class AnalysisConfig:
    """Analiz ayarları"""
    decimal_precision: int = 4
    use_optimal_bbox: bool = True  # True: Optimal OBB, False: Axis-aligned
    calculate_inertia: bool = True
    name_search_depth: int = 10  # Parent zinciri derinliği


@dataclass
class OutputConfig:
    """Çıktı ayarları"""
    output_dir: str = "."
    save_json: bool = True
    save_detailed_debug: bool = True
    json_filename: Optional[str] = None  # None = otomatik
    include_name_candidates: bool = True  # İsim aday listesini dahil et


@dataclass
class UnitsConfig:
    """Birim ayarları"""
    length_unit: str = "mm"
    length_multiplier: float = 1.0
    area_unit: str = "mm²"
    area_multiplier: float = 1.0
    volume_unit: str = "mm³"
    volume_multiplier: float = 1.0
    weight_unit: str = "g"
    weight_multiplier: float = 1.0


# ============================================================================
# FREECAD ANALİZ MOTORU
# ============================================================================

class FreeCADAnalyzer:
    """FreeCAD model analiz motoru"""

    def __init__(self,
                 material_cfg: MaterialConfig,
                 analysis_cfg: AnalysisConfig,
                 output_cfg: OutputConfig,
                 units_cfg: UnitsConfig):
        self.material_cfg = material_cfg
        self.analysis_cfg = analysis_cfg
        self.output_cfg = output_cfg
        self.units_cfg = units_cfg

        self.doc = None
        self.results: List[Dict] = []

    def debug(self, msg: str, level: str = "INFO"):
        """Debug mesajı"""
        if self.output_cfg.save_detailed_debug or level in ["ERROR", "WARNING"]:
            print(f"[{level}] {msg}", flush=True)

    def around(self, value: float) -> float:
        """Sayıyı belirtilen hassasiyete yuvarla"""
        if self.analysis_cfg.decimal_precision == -1:
            return value
        return round(value, self.analysis_cfg.decimal_precision)

    def find_object_name(self, obj, max_depth=10, debug=True):
        """
        Derin arama ile FreeCAD nesnesinin 'gerçek' ismini bulur.
        Return: (real_name, source_key, candidates_dict)
        - candidates_dict: {key: {"value": val, "score":score, "path": path_list}}
        """
        from collections import deque

        def dprint(*a, **k):
            if debug:
                print("[DEBUG]", *a, **k)

        def safe_get(o, attr):
            try:
                return getattr(o, attr)
            except Exception as e:
                return None

        # scoring: daha yüksek olan tercih edilir
        SCORE = {
            "Label": 100,
            "Label2": 90,
            "LinkedObject.Label": 95,
            "LinkedObject.Name": 85,
            "ParentGroup.Label": 80,
            "InList.Label": 75,
            "OutListRecursive.Label": 75,
            "Parents.Label": 70,
            "FullName": 60,
            "OldLabel": 50,
            "Name": 40,
            "Shape.Tag": 30,
            "ViewObject.DisplayName": 65,
            "ViewObject.Label": 65,
        }

        candidates = {}  # key -> {"value":..., "score":..., "path":[(repr,obj_type), ...]}

        visited = set()  # store unique ids to avoid cycles

        def record(key, val, score, path):
            if not val:
                return
            prev = candidates.get(key)
            if prev is None or score > prev["score"]:
                candidates[key] = {"value": val, "score": score, "path": list(path)}
                dprint(f"[CANDIDATE] {key} = {val} (score={score}) via {path}")

        # BFS stack with (current_obj, depth, path_description_list)
        q = deque()
        q.append((obj, 0, [("start", safe_get(obj, "Name") or safe_get(obj, "Label") or str(obj.__class__.__name__))]))

        while q:
            current, depth, path = q.popleft()
            try:
                uid = (safe_get(current, "Document").Name if safe_get(current, "Document") else None, safe_get(current, "Name") or id(current))
            except Exception:
                uid = (None, id(current))
            if uid in visited:
                dprint(f"[SKIP] already visited {uid}")
                continue
            visited.add(uid)

            dprint(f"\n[VISIT] depth={depth} obj={current} type={getattr(current,'TypeId', current.__class__.__name__)}")

            # 1) Basic attributes
            for attr in ["Label", "Label2", "OldLabel", "FullName", "Name"]:
                try:
                    val = safe_get(current, attr)
                except Exception as e:
                    val = None
                dprint(f"[PROP] {attr} = {val}")
                if val:
                    score_key = attr
                    score = SCORE.get(score_key, 10)
                    record(score_key, val, score, path + [(attr, val)])

            # 2) ViewObject possibilities
            vo = safe_get(current, "ViewObject")
            if vo:
                for vattr, keyname in [("DisplayName", "ViewObject.DisplayName"), ("Label", "ViewObject.Label")]:
                    try:
                        vval = getattr(vo, vattr, None)
                    except Exception:
                        vval = None
                    dprint(f"[VIEW] {vattr} = {vval}")
                    if vval:
                        record(keyname, vval, SCORE.get(keyname, 65), path + [("ViewObject."+vattr, vval)])

            # 3) Shape tag
            shp = safe_get(current, "Shape")
            if shp:
                try:
                    stag = getattr(shp, "Tag", None)
                except Exception:
                    stag = None
                dprint(f"[SHAPE] Tag = {stag}")
                if stag:
                    record("Shape.Tag", str(stag), SCORE.get("Shape.Tag", 30), path + [("Shape.Tag", stag)])

            # 4) getLinkedObject()
            if hasattr(current, "getLinkedObject"):
                try:
                    linked = current.getLinkedObject()
                except Exception as e:
                    linked = None
                dprint(f"[LINKED] -> {linked}")
                if linked and linked != current:
                    # record linked's label/name
                    llabel = safe_get(linked, "Label") or safe_get(linked, "Name")
                    if llabel:
                        record("LinkedObject.Label", llabel, SCORE.get("LinkedObject.Label", 95), path + [("Linked", llabel)])
                    q.append((linked, depth + 1, path + [("linked", safe_get(linked, "Name") or safe_get(linked, "Label") or str(linked.__class__.__name__))]))

            # 5) InList (parents/groups)
            inlist = safe_get(current, "InList")
            if inlist:
                try:
                    for i, parent in enumerate(inlist):
                        dprint(f"[INLIST] idx={i} -> {parent}")
                        plabel = safe_get(parent, "Label") or safe_get(parent, "Name")
                        if plabel:
                            record(f"InList[{i}].Label", plabel, SCORE.get("InList.Label", 75), path + [(f"InList[{i}]", plabel)])
                        q.append((parent, depth + 1, path + [(f"InList[{i}]", plabel or str(parent.__class__.__name__))]))
                except Exception as e:
                    dprint(f"[ERR] InList read error: {e}")

            # 6) OutListRecursive (çocukların referansından en üst grubu bulma)
            if hasattr(current, "OutListRecursive"):
                try:
                    olr = safe_get(current, "OutListRecursive")
                except Exception:
                    olr = None
                if olr:
                    try:
                        for i, child in enumerate(olr):
                            dprint(f"[OUTREC] idx={i} -> {child}")
                            clabel = safe_get(child, "Label") or safe_get(child, "Name")
                            if clabel:
                                record(f"OutListRecursive[{i}].Label", clabel, SCORE.get("OutListRecursive.Label", 75), path + [(f"OutRec[{i}]", clabel)])
                            # explore child a bit (but avoid flooding): append with depth+1
                            if depth + 1 <= max_depth:
                                q.append((child, depth + 1, path + [(f"OutRec[{i}]", clabel or str(child.__class__.__name__))]))
                    except Exception as e:
                        dprint(f"[ERR] OutListRecursive error: {e}")

            # 7) Parents (çoğul)
            parents_prop = safe_get(current, "Parents")
            if parents_prop:
                try:
                    for i, parent_tuple in enumerate(parents_prop):
                        # parent_tuple bazen (obj,prop) şeklinde olabilir
                        parent_obj = parent_tuple[0] if isinstance(parent_tuple, (list, tuple)) and parent_tuple else parent_tuple
                        dprint(f"[PARENTS] idx={i} -> {parent_obj}")
                        plabel = safe_get(parent_obj, "Label") or safe_get(parent_obj, "Name")
                        if plabel:
                            record(f"Parents[{i}].Label", plabel, SCORE.get("Parents.Label", 70), path + [(f"Parents[{i}]", plabel)])
                        if depth + 1 <= max_depth:
                            q.append((parent_obj, depth + 1, path + [(f"Parents[{i}]", plabel or str(parent_obj.__class__.__name__))]))
                except Exception as e:
                    dprint(f"[ERR] Parents read error: {e}")

            # 8) getParent() if available
            if hasattr(current, "getParent"):
                try:
                    parent = current.getParent()
                except Exception:
                    parent = None
                dprint(f"[GETPARENT] -> {parent}")
                if parent and parent != current:
                    plabel = safe_get(parent, "Label") or safe_get(parent, "Name")
                    if plabel:
                        record("Parent.Label", plabel, SCORE.get("ParentGroup.Label", 80), path + [("Parent", plabel)])
                    if depth + 1 <= max_depth:
                        q.append((parent, depth + 1, path + [("parent", plabel or str(parent.__class__.__name__))]))

            # 9) getParentGroup()
            if hasattr(current, "getParentGroup"):
                try:
                    pg = current.getParentGroup()
                except Exception:
                    pg = None
                dprint(f"[PARENTGROUP] -> {pg}")
                if pg and pg != current:
                    pgl = safe_get(pg, "Label") or safe_get(pg, "Name")
                    if pgl:
                        record("ParentGroup.Label", pgl, SCORE.get("ParentGroup.Label", 80), path + [("ParentGroup", pgl)])
                    if depth + 1 <= max_depth:
                        q.append((pg, depth + 1, path + [("ParentGroup", pgl or str(pg.__class__.__name__))]))

            # stop if depth reached
            if depth >= max_depth:
                dprint(f"[DEPTH] Reached max_depth at obj {current}")
                continue

        # --- seçim: en yüksek skorlu candidate ---
        if not candidates:
            dprint("[RESULT] Hiç aday bulunamadı.")
            return "Unknown", "None", candidates

        # find max score
        best_key, best_info = max(candidates.items(), key=lambda kv: kv[1]["score"])
        dprint("\n[FINAL] Tüm adaylar (key: value (score)):")
        for k, v in sorted(candidates.items(), key=lambda kv: -kv[1]["score"]):
            dprint(f"  - {k}: {v['value']} (score={v['score']}) path={v.get('path')}")
        dprint(f"\n[FINAL] Seçilen: {best_key} => {best_info['value']} (score={best_info['score']})")
        return best_info["value"], best_key, candidates




    def calculate_bounding_box(self, obj) -> Tuple[Dict, Dict]:
        """
        Bounding box hesapla
        Returns: (optimal_bbox, axis_aligned_bbox)
        """
        optimal_bbox = {}
        axis_aligned_bbox = {}

        # Axis-aligned bbox
        try:
            bb_aa = obj.Shape.BoundBox
            axis_aligned_bbox = {
                'x_length': self.around(bb_aa.XLength * self.units_cfg.length_multiplier),
                'y_length': self.around(bb_aa.YLength * self.units_cfg.length_multiplier),
                'z_length': self.around(bb_aa.ZLength * self.units_cfg.length_multiplier),
                'diagonal': self.around(bb_aa.DiagonalLength * self.units_cfg.length_multiplier),
                'x_min': self.around(bb_aa.XMin),
                'y_min': self.around(bb_aa.YMin),
                'z_min': self.around(bb_aa.ZMin),
                'x_max': self.around(bb_aa.XMax),
                'y_max': self.around(bb_aa.YMax),
                'z_max': self.around(bb_aa.ZMax),
                'type': 'axis_aligned',
                'unit': self.units_cfg.length_unit
            }
        except Exception as e:
            self.debug(f"Axis-aligned bbox hatası: {e}", "WARNING")

        # Optimal oriented bbox
        if self.analysis_cfg.use_optimal_bbox:
            try:
                import Part
                obb = obj.Shape.optimalBoundingBox()
                optimal_bbox = {
                    'x_length': self.around(obb.XLength * self.units_cfg.length_multiplier),
                    'y_length': self.around(obb.YLength * self.units_cfg.length_multiplier),
                    'z_length': self.around(obb.ZLength * self.units_cfg.length_multiplier),
                    'diagonal': self.around(obb.DiagonalLength * self.units_cfg.length_multiplier),
                    'x_min': self.around(obb.XMin),
                    'y_min': self.around(obb.YMin),
                    'z_min': self.around(obb.ZMin),
                    'x_max': self.around(obb.XMax),
                    'y_max': self.around(obb.YMax),
                    'z_max': self.around(obb.ZMax),
                    'type': 'optimal_oriented',
                    'unit': self.units_cfg.length_unit
                }
            except Exception as e:
                self.debug(f"Optimal bbox hatası: {e}", "WARNING")
                optimal_bbox = axis_aligned_bbox.copy()
                optimal_bbox['type'] = 'fallback_to_axis_aligned'

        return optimal_bbox, axis_aligned_bbox

    def analyze_object(self, obj) -> Optional[Dict]:
        """Tek bir nesneyi analiz et"""
        try:
            self.debug(f"Analiz ediliyor: {obj.Name}")

            # Shape kontrolü
            if not hasattr(obj, "Shape"):
                self.debug(f"Shape yok, atlanıyor: {obj.Name}", "WARNING")
                return None

            # İsim bul
            real_name, name_source, name_candidates = self.find_object_name(obj)

            # Yüzey alanı
            try:
                surface = obj.Shape.Area * self.units_cfg.area_multiplier
            except:
                surface = 0.0

            # Hacim
            try:
                volume = obj.Shape.Volume * self.units_cfg.volume_multiplier
            except:
                volume = 0.0

            # Ağırlık hesaplama
            weight_g = ((volume * self.material_cfg.density_kg_dm3) * self.units_cfg.weight_multiplier) / 1000.0

            # Bounding box
            optimal_bbox, axis_aligned_bbox = self.calculate_bounding_box(obj)

            # Matrix of Inertia
            inertia = None
            if self.analysis_cfg.calculate_inertia:
                try:
                    matrix = obj.Shape.MatrixOfInertia
                    inertia = {
                        'raw': str(matrix),
                        'A11': matrix.A11, 'A12': matrix.A12, 'A13': matrix.A13, 'A14': matrix.A14,
                        'A21': matrix.A21, 'A22': matrix.A22, 'A23': matrix.A23, 'A24': matrix.A24,
                        'A31': matrix.A31, 'A32': matrix.A32, 'A33': matrix.A33, 'A34': matrix.A34,
                        'A41': matrix.A41, 'A42': matrix.A42, 'A43': matrix.A43, 'A44': matrix.A44,
                    }
                except:
                    inertia = {'error': 'Could not calculate inertia'}

            # Sonuç objesi
            result = {
                'object_name': real_name,
                'object_name_source': name_source,
                'object_name_internal': obj.Name if hasattr(obj, 'Name') else 'Unknown',
                'object_label': obj.Label if hasattr(obj, 'Label') else obj.Name,
                'object_type': obj.TypeId if hasattr(obj, 'TypeId') else 'Unknown',

                'bounding_box_optimal': optimal_bbox if optimal_bbox else axis_aligned_bbox,
                'bounding_box_axis_aligned': axis_aligned_bbox,

                'surface': {
                    'value': self.around(surface),
                    'unit': self.units_cfg.area_unit
                },

                'volume': {
                    'value': self.around(volume),
                    'unit': self.units_cfg.volume_unit,
                    'cm3': self.around(volume / 1000.0) if self.units_cfg.volume_unit == 'mm³' else None
                },

                'weight': {
                    'value': self.around(weight_g),
                    'unit': self.units_cfg.weight_unit,
                    'kg': self.around(weight_g / 1000.0),
                    'material': self.material_cfg.name,
                    'density_kg_dm3': self.material_cfg.density_kg_dm3
                }
            }

            # İsteğe bağlı alanlar
            if self.analysis_cfg.calculate_inertia:
                result['matrix_of_inertia'] = inertia

            if self.output_cfg.include_name_candidates:
                result['name_search_results'] = name_candidates

            return result

        except Exception as e:
            self.debug(f"Nesne analiz hatası ({obj.Name}): {e}", "ERROR")
            import traceback
            if self.output_cfg.save_detailed_debug:
                traceback.print_exc()
            return None

    def analyze_file(self, file_path: str) -> Dict:
        """STEP dosyasını analiz et"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Dosya bulunamadı: {file_path}")

        self.debug(f"Dosya açılıyor: {file_path}")

        try:
            # STEP dosyasını import et
            import Part
            self.doc = FreeCAD.newDocument("AnalysisDoc")
            Part.insert(file_path, self.doc.Name)

            objects = self.doc.Objects
            self.debug(f"Toplam {len(objects)} nesne bulundu")

            if not objects:
                raise ValueError("Dosyada nesne bulunamadı")

            # Her nesneyi analiz et
            results = []
            total_weight = 0.0

            for i, obj in enumerate(objects):
                self.debug(f"[{i+1}/{len(objects)}] {obj.Name}")
                result = self.analyze_object(obj)
                if result:
                    results.append(result)
                    total_weight += result['weight']['value']

            # Toplam özet
            summary = {
                'file_path': str(Path(file_path).absolute()),
                'file_name': Path(file_path).name,
                'total_objects_found': len(objects),
                'total_objects_analyzed': len(results),
                'material': {
                    'name': self.material_cfg.name,
                    'density_kg_dm3': self.material_cfg.density_kg_dm3,
                    'symbol': self.material_cfg.symbol
                },
                'total_weight': {
                    'value': self.around(total_weight),
                    'unit': self.units_cfg.weight_unit,
                    'kg': self.around(total_weight / 1000.0)
                },
                'analysis_config': {
                    'decimal_precision': self.analysis_cfg.decimal_precision,
                    'use_optimal_bbox': self.analysis_cfg.use_optimal_bbox,
                    'calculate_inertia': self.analysis_cfg.calculate_inertia
                },
                'units': {
                    'length': self.units_cfg.length_unit,
                    'area': self.units_cfg.area_unit,
                    'volume': self.units_cfg.volume_unit,
                    'weight': self.units_cfg.weight_unit
                },
                'objects': results
            }

            # JSON kaydet
            if self.output_cfg.save_json:
                json_path = self._save_json(file_path, summary)
                summary['json_output_path'] = json_path

            # Belgeyi kapat
            FreeCAD.closeDocument(self.doc.Name)
            self.debug("Analiz tamamlandı", "SUCCESS")

            return summary

        except Exception as e:
            self.debug(f"Dosya analiz hatası: {e}", "ERROR")
            if self.doc:
                FreeCAD.closeDocument(self.doc.Name)
            raise

    def _save_json(self, input_file: str, data: Dict) -> str:
        """JSON dosyasına kaydet"""
        if self.output_cfg.json_filename:
            json_path = os.path.join(self.output_cfg.output_dir, self.output_cfg.json_filename)
        else:
            base_name = Path(input_file).stem
            json_path = os.path.join(self.output_cfg.output_dir, f"{base_name}_analysis.json")

        os.makedirs(self.output_cfg.output_dir, exist_ok=True)

        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

        self.debug(f"JSON kaydedildi: {json_path}")
        return json_path


# ============================================================================
# YARDIMCI FONKSİYONLAR
# ============================================================================

def load_config_from_json(json_path: str) -> Dict:
    """JSON'dan konfigürasyon yükle"""
    with open(json_path, 'r', encoding='utf-8') as f:
        return json.load(f)


def save_config_template(output_path: str):
    """Örnek konfigürasyon şablonu oluştur"""
    config = {
        'input_file': 'model.stp',
        'material': asdict(MaterialConfig()),
        'analysis': asdict(AnalysisConfig()),
        'output': asdict(OutputConfig()),
        'units': asdict(UnitsConfig())
    }

    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=2, ensure_ascii=False)

    print(f"Konfigürasyon şablonu oluşturuldu: {output_path}")


def print_summary(summary: Dict):
    """Özet raporu yazdır"""
    print("\n" + "="*70)
    print("ANALİZ RAPORU")
    print("="*70)
    print(f"Dosya           : {summary['file_name']}")
    print(f"Toplam Nesne    : {summary['total_objects_analyzed']}/{summary['total_objects_found']}")
    print(f"Malzeme         : {summary['material']['name']} ({summary['material']['symbol']})")
    print(f"Yoğunluk        : {summary['material']['density_kg_dm3']} kg/dm³")
    print(f"Toplam Ağırlık  : {summary['total_weight']['value']} {summary['total_weight']['unit']}")
    print(f"                : {summary['total_weight']['kg']} kg")
    print("="*70)

    for i, obj in enumerate(summary['objects'], 1):
        print(f"\n[{i}] {obj['object_name']} ({obj['object_label']})")
        print(f"    Tip    : {obj['object_type']}")

        bbox = obj['bounding_box_optimal']
        print(f"    BBox   : X={bbox['x_length']} Y={bbox['y_length']} Z={bbox['z_length']} {bbox['unit']}")
        print(f"    Hacim  : {obj['volume']['value']} {obj['volume']['unit']}")
        print(f"    Ağırlık: {obj['weight']['value']} {obj['weight']['unit']}")

    print("\n" + "="*70 + "\n")


# ============================================================================
# MALZEME ÖN TANIMLARI
# ============================================================================

MATERIALS = {
    'aluminium': MaterialConfig(name='Aluminium', density_kg_dm3=2.71, symbol='Al'),
    'steel': MaterialConfig(name='Steel', density_kg_dm3=7.85, symbol='Fe'),
    'copper': MaterialConfig(name='Copper', density_kg_dm3=8.96, symbol='Cu'),
    'titanium': MaterialConfig(name='Titanium', density_kg_dm3=4.51, symbol='Ti'),
    'plastic_pla': MaterialConfig(name='PLA Plastic', density_kg_dm3=1.25, symbol='PLA'),
    'plastic_abs': MaterialConfig(name='ABS Plastic', density_kg_dm3=1.05, symbol='ABS'),
    'wood': MaterialConfig(name='Wood (Oak)', density_kg_dm3=0.75, symbol='Wood'),
    'glass': MaterialConfig(name='Glass', density_kg_dm3=2.50, symbol='Glass'),
    'bronze': MaterialConfig(name='Bronze', density_kg_dm3=8.80, symbol='Bronze'),
    'brass': MaterialConfig(name='Brass', density_kg_dm3=8.50, symbol='Brass'),
}


# ============================================================================
# MAIN - CLI VE WEB API UYUMLU
# ============================================================================

def main():
    parser = argparse.ArgumentParser(
        description="FreeCAD Model Analyzer API - Parametrik Analiz Aracı",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Örnekler:
  # Basit kullanım
  freecadcmd free.py model.stp

  # JSON config ile
  freecadcmd free.py -c analysis-config.json

  # Farklı malzeme
  freecadcmd free.py model.stp --material steel

  # Özel yoğunluk
  freecadcmd free.py model.stp --density 7.85

  # Çıktı dizini
  freecadcmd free.py model.stp -o ./results

  # Config şablonu oluştur
  freecadcmd free.py --create-config analysis-config.json
        """
    )

    parser.add_argument('input_file', nargs='?', help='STEP dosya yolu (.stp/.step)')
    parser.add_argument('-c', '--config', help='JSON konfigürasyon dosyası')
    parser.add_argument('-o', '--output', help='Çıktı dizini')
    parser.add_argument('--material', choices=list(MATERIALS.keys()), help='Malzeme seçimi')
    parser.add_argument('--density', type=float, help='Özel malzeme yoğunluğu (kg/dm³)')
    parser.add_argument('--precision', type=int, help='Ondalık hassasiyet')
    parser.add_argument('--no-json', action='store_true', help='JSON kaydetme')
    parser.add_argument('--no-optimal-bbox', action='store_true', help='Sadece axis-aligned bbox kullan')
    parser.add_argument('--no-inertia', action='store_true', help='İnertia hesaplama')
    parser.add_argument('--include-candidates', action='store_true', help='İsim aday listesini dahil et')
    parser.add_argument('--create-config', help='Örnek config.json oluştur')
    parser.add_argument('--list-materials', action='store_true', help='Mevcut malzemeleri listele')
    parser.add_argument('--debug', action='store_true', help='Detaylı debug çıktısı')

    args = parser.parse_args()

    # Malzeme listesi
    if args.list_materials:
        print("\nMevcut Malzemeler:")
        print("="*60)
        for key, mat in MATERIALS.items():
            print(f"  {key:15s} : {mat.name:20s} ({mat.symbol}) - {mat.density_kg_dm3} kg/dm³")
        print("="*60 + "\n")
        return

    # Config şablonu oluştur
    if args.create_config:
        save_config_template(args.create_config)
        return

    # Konfigürasyon yükle
    if args.config:
        config_data = load_config_from_json(args.config)
        input_file = config_data.get('input_file', args.input_file)
        material_cfg = MaterialConfig(**config_data.get('material', {}))
        analysis_cfg = AnalysisConfig(**config_data.get('analysis', {}))
        output_cfg = OutputConfig(**config_data.get('output', {}))
        units_cfg = UnitsConfig(**config_data.get('units', {}))
    else:
        # Varsayılan değerler
        input_file = args.input_file
        material_cfg = MATERIALS['aluminium']
        analysis_cfg = AnalysisConfig()
        output_cfg = OutputConfig()
        units_cfg = UnitsConfig()

    # CLI override
    if args.material:
        material_cfg = MATERIALS[args.material]
    if args.density:
        material_cfg.density_kg_dm3 = args.density
        material_cfg.name = f"Custom ({args.density} kg/dm³)"
    if args.output:
        output_cfg.output_dir = args.output
    if args.precision is not None:
        analysis_cfg.decimal_precision = args.precision
    if args.no_json:
        output_cfg.save_json = False
    if args.no_optimal_bbox:
        analysis_cfg.use_optimal_bbox = False
    if args.no_inertia:
        analysis_cfg.calculate_inertia = False
    if args.include_candidates:
        output_cfg.include_name_candidates = True
    if args.debug:
        output_cfg.save_detailed_debug = True

    # Dosya kontrolü
    if not input_file:
        parser.print_help()
        print("\n[HATA] STEP dosyası belirtilmedi!")
        return

    if not os.path.exists(input_file):
        print(f"\n[HATA] Dosya bulunamadı: {input_file}\n")
        return

    # Analiz et
    try:
        analyzer = FreeCADAnalyzer(
            material_cfg,
            analysis_cfg,
            output_cfg,
            units_cfg
        )

        summary = analyzer.analyze_file(input_file)

        # Rapor yazdır
        print_summary(summary)

        # JSON yolu
        if output_cfg.save_json:
            print(f"JSON çıktısı: {summary.get('json_output_path', 'N/A')}\n")

    except Exception as e:
        print(f"\n[HATA] Analiz başarısız: {e}\n")
        import traceback
        if args.debug:
            traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
