# app.py
import inspect
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Callable

import tkinter as tk
from tkinter import filedialog, messagebox, ttk

try:
    import pandas as pd
except ImportError:
    pd = None

try:
    from core.crbx_sia import parse_lv_crbx, parse_offer_crbx
except ImportError:
    parse_lv_crbx = None
    parse_offer_crbx = None

try:
    import ai_gemini_parser as aip
except ImportError:
    aip = None

try:
    from exports.export_xlsx import export_xlsx
except ImportError:
    export_xlsx = None

@dataclass
class LVMeta:
    path: Path
    kind: str
    info: str = ""
    positions: Dict[str, Dict[str, Any]] = field(default_factory=dict)

@dataclass
class Offer:
    vendor: str
    kind: str
    path: Path
    active: bool = True
    positions: Dict[str, Dict[str, Any]] = field(default_factory=dict)

def _num(x: Any) -> Optional[float]:
    if x is None: return None
    if isinstance(x, (int, float)): return abs(float(x))
    s = str(x).strip().replace("'", "").replace(" ", "").replace("’", "").replace("\xa0", "").replace(",", ".")
    s = s.replace("–", "-").replace("—", "-")
    try:
        return abs(float(s))
    except (ValueError, TypeError):
        return None

def _safe_call(func: Optional[Callable], **kwargs: Any) -> Any:
    if func is None: raise RuntimeError("Benötigte Funktion nicht verfügbar.")
    sig = inspect.signature(func)
    return func(**{k: v for k, v in kwargs.items() if k in sig.parameters})

def _detect_pdf_parsers() -> Tuple[Optional[Callable], Optional[Callable]]:
    if aip is None: return None, None
    return getattr(aip, "extract_offer_from_pdf", None), getattr(aip, "extract_lv_from_pdf", None)

def _normalize_offer_map(raw: Any) -> Dict[str, Dict[str, Any]]:
    out: Dict[str, Dict[str, Any]] = {}
    if not isinstance(raw, dict): return out
    for pos, data in raw.items():
        if isinstance(data, dict):
            out[pos] = {"ep": _num(data.get("ep")), "gp": _num(data.get("gp"))}
    return out

def _normalize_lv_positions(raw: Any) -> Dict[str, Dict[str, Any]]:
    out: Dict[str, Dict[str, Any]] = {}
    if not isinstance(raw, dict): return out
    for pos, data in raw.items():
        if isinstance(data, dict):
            out[pos] = {
                "qty": _num(data.get("qty")),
                "unit": str(data.get("unit") or "").strip() or None,
                "text": str(data.get("text") or "").strip(),
            }
    return out

class App(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.title("ERNE Offertvergleich"); self.geometry("980x560")
        self.lv: Optional[LVMeta] = None
        self.offers: List[Offer] = []
        self.last_offer_positions: Dict[str, Dict[str, Dict[str, Any]]] = {}
        self.offer_pdf_parser, self.lv_pdf_parser = _detect_pdf_parsers()
        self._build_ui()

    def _build_ui(self) -> None:
        root = ttk.Frame(self, padding=8); root.pack(fill="both", expand=True)
        gb_lv = ttk.LabelFrame(root, text="Leistungsverzeichnis (LV)"); gb_lv.pack(fill="x", padx=4, pady=4)
        lv_btns = ttk.Frame(gb_lv); lv_btns.pack(fill="x", pady=(4, 6))
        ttk.Button(lv_btns, text="LV laden (CRBX)", command=self._load_lv_crbx).pack(side="left", padx=(0, 6))
        ttk.Button(lv_btns, text="LV laden (PDF)", command=self._load_lv_pdf).pack(side="left", padx=(0, 6))
        ttk.Button(lv_btns, text="LV entfernen", command=self._clear_lv).pack(side="left", padx=(0, 6))
        self.lv_info_var = tk.StringVar(value="Kein LV geladen."); ttk.Label(gb_lv, textvariable=self.lv_info_var).pack(fill="x", padx=4, pady=(0, 4))
        
        gb_off = ttk.LabelFrame(root, text="Offerten"); gb_off.pack(fill="both", expand=True, padx=4, pady=4)
        off_btns = ttk.Frame(gb_off); off_btns.pack(fill="x", pady=(4, 6))
        ttk.Button(off_btns, text="Offerte(n) laden", command=self._load_offers).pack(side="left", padx=(0, 6))
        ttk.Button(off_btns, text="Auswahl entfernen", command=self._remove_selected_offers).pack(side="left", padx=(0, 6))
        
        self.tv = ttk.Treeview(gb_off, columns=("aktiv", "unternehmer", "typ", "datei"), show="headings", selectmode="extended", height=9)
        self.tv.heading("aktiv", text="Aktiv"); self.tv.column("aktiv", width=60, anchor="center")
        self.tv.heading("unternehmer", text="Unternehmer"); self.tv.column("unternehmer", width=220)
        self.tv.heading("typ", text="Typ"); self.tv.column("typ", width=70, anchor="center")
        self.tv.heading("datei", text="Datei"); self.tv.column("datei", width=560)
        self.tv.pack(fill="both", expand=True, padx=4, pady=(0, 6))
        self.tv.bind("<Double-1>", self._toggle_active_on_dclick); self.tv.bind("<space>", self._toggle_active_on_space)
        
        act = ttk.Frame(root); act.pack(fill="x", padx=4, pady=4)
        ttk.Button(act, text="Offertanalyse starten", command=self._run_full_analysis).pack(side="left", padx=(0, 6))
        ttk.Button(act, text="Analyse zurücksetzen", command=self._clear_analysis_cache).pack(side="left", padx=(0, 18))
        ttk.Button(act, text="XLSX exportieren", command=self._export_xlsx).pack(side="left", padx=(0, 6))
        
        self.status_var = tk.StringVar(value="Bereit."); ttk.Label(root, textvariable=self.status_var, relief="groove").pack(fill="x", padx=4, pady=(6, 0))
        self._refresh_status()

    def _refresh_status(self):
        if self.lv: self.lv_info_var.set(f"{self.lv.path.name} | Typ: {self.lv.kind} | Pos: {len(self.lv.positions)}")
        else: self.lv_info_var.set("Kein LV geladen.")
        self.tv.delete(*self.tv.get_children())
        for off in self.offers: self.tv.insert("", "end", iid=str(id(off)), values=("x" if off.active else "", off.vendor, off.kind, off.path.name))
        num_analyzed = len(self.last_offer_positions)
        status_text = f"{len(self.offers)} Offerten geladen. {sum(1 for o in self.offers if o.active)} aktiv."
        if num_analyzed > 0: status_text += f" {num_analyzed} analysiert."
        self.status_var.set(status_text)

    def _clear_lv(self):
        self.lv = None; self._clear_analysis_cache()

    def _clear_analysis_cache(self):
        self.last_offer_positions.clear()
        for off in self.offers: off.positions.clear()
        messagebox.showinfo("Zurückgesetzt", "Alle Analyseergebnisse wurden entfernt.")
        self._refresh_status()

    def _load_lv_pdf(self):
        path = filedialog.askopenfilename(title="LV als PDF laden", filetypes=[("PDF", "*.pdf")])
        if not path: return
        try:
            self.status_var.set("Analysiere LV-PDF mit AI..."); self.update_idletasks()
            positions = _normalize_lv_positions(_safe_call(self.lv_pdf_parser, pdf_path=Path(path)))
            if not positions: raise ValueError("Keine Positionen gefunden.")
            self.lv = LVMeta(path=Path(path), kind="PDF", positions=positions)
        except Exception as ex: messagebox.showerror("Fehler", f"LV aus PDF lesen fehlgeschlagen:\n{ex}")
        finally: self._refresh_status()

    def _load_lv_crbx(self):
        path = filedialog.askopenfilename(title="LV als CRBX/SIA451 laden", filetypes=[("CRBX/SIA451", "*.crbx *.01s *.e1s *.zip")])
        if not path: return
        try:
            positions = _normalize_lv_positions(_safe_call(parse_lv_crbx, crbx_path=Path(path)))
            if not positions: raise ValueError("Keine Positionen gefunden.")
            self.lv = LVMeta(path=Path(path), kind="CRBX", positions=positions)
        except Exception as ex: messagebox.showerror("Fehler", f"LV aus CRBX lesen fehlgeschlagen:\n{ex}")
        self._refresh_status()

    def _load_offers(self):
        paths = filedialog.askopenfilenames(title="Offerte(n) laden", filetypes=[("PDF/CRBX", "*.pdf *.crbx *.01s *.e1s *.zip")])
        for path in paths:
            p = Path(path)
            kind = "PDF" if p.suffix.lower() == ".pdf" else "CRBX"
            vendor = re.sub(r"(?i)\b(angebot|offerte)\b.*$", "", p.stem).strip().replace("_", " ") or p.stem
            self.offers.append(Offer(vendor=vendor, kind=kind, path=p))
        self._refresh_status()

    def _remove_selected_offers(self):
        selected_ids = set(self.tv.selection())
        if selected_ids: self.offers = [o for o in self.offers if str(id(o)) not in selected_ids]; self._refresh_status()

    def _toggle_active(self, item_id: str):
        if not item_id: return
        for off in self.offers:
            if str(id(off)) == item_id: off.active = not off.active; break
        self._refresh_status()
    def _toggle_active_on_dclick(self, e): self._toggle_active(self.tv.identify_row(e.y))
    def _toggle_active_on_space(self, e): [self._toggle_active(i) for i in self.tv.selection()]

    def _run_full_analysis(self):
        if not self.lv: return messagebox.showwarning("Fehlt", "Bitte zuerst ein LV laden.")
        active_offers = [o for o in self.offers if o.active]
        if not active_offers: return messagebox.showwarning("Fehlt", "Keine Offerten aktiv.")
        
        self.last_offer_positions.clear(); errs = []
        for i, off in enumerate(active_offers):
            try:
                self.status_var.set(f"Analysiere {i+1}/{len(active_offers)}: {off.path.name}..."); self.update_idletasks()
                if off.kind == "PDF":
                    raw = _safe_call(self.offer_pdf_parser, pdf_path=off.path, lv_posmap=self.lv.positions)
                else: # CRBX
                    _, raw = _safe_call(parse_offer_crbx, crbx_path=off.path)
                    if not any(v.get('ep') or v.get('gp') for v in raw.values()):
                        errs.append(f"Datei '{off.path.name}': Enthält keine Preisdaten (wahrscheinlich eine Ausschreibung).")
                        raw = {}
                off.positions = _normalize_offer_map(raw)
            except Exception as ex: errs.append(f"Fehler bei '{off.vendor}': {ex}")
        
        self.last_offer_positions = {o.vendor: o.positions for o in active_offers}
        if errs: messagebox.showwarning("Analyse mit Warnungen", "\n\n".join(errs))
        else: messagebox.showinfo("OK", "Offertanalyse erfolgreich.")
        self._refresh_status()

    def _export_xlsx(self):
        if not self.lv or not self.last_offer_positions:
            return messagebox.showwarning("Fehlende Daten", "Bitte zuerst LV laden und Offertanalyse starten.")
        
        path = filedialog.asksaveasfilename(title="Preisspiegel als XLSX speichern", defaultextension=".xlsx", filetypes=[("Excel", "*.xlsx")])
        if not path: return
        
        try:
            self.status_var.set("Erstelle XLSX-Datei..."); self.update_idletasks()
            _safe_call(export_xlsx, out_path=Path(path), lv_meta=self.lv, offer_positions=self.last_offer_positions)
            messagebox.showinfo("OK", f"XLSX erfolgreich exportiert:\n{Path(path).name}")
        except Exception as ex:
            messagebox.showerror("Fehler", f"XLSX Export fehlgeschlagen:\n{ex}")
        finally:
            self._refresh_status()

if __name__ == "__main__":
    app = App()
    app.mainloop()