#!/usr/bin/env python3
"""
data_processor.py

Zentrale Klasse zur Orchestrierung und Verarbeitung von Unternehmensdaten.
Enthält die Logik für die Verarbeitung einzelner Zeilen sowie die Steuerung
verschiedener Batch-Modi und Dienstprogramme.
"""

__version__ = "v2.0.4"

import logging
import time
import traceback
import concurrent.futures
import threading
import pickle
import json
import os
import re
from datetime import datetime

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
from concurrent.futures import ThreadPoolExecutor, as_completed

# Import der abhängigen Module
from config import Config, COLUMN_MAP, MODEL_FILE, IMPUTER_FILE, PATTERNS_FILE_JSON
from helpers import (
    retry_on_failure,
    serp_website_lookup,
    get_website_raw,
    scrape_website_details,
    summarize_website_content,
    URL_CHECK_MARKER,
    evaluate_branche_chatgpt,
    get_numeric_filter_value,
    initialize_target_schema,
    search_linkedin_contacts,
    is_valid_wikipedia_article_url,
    verify_wiki_article_chatgpt,
    generate_fsm_pitch,
    get_col_idx,
    _get_col_letter,
    summarize_wikipedia_article,
    evaluate_branches_batch
)
# Klassen-Imports
from google_sheet_handler import GoogleSheetHandler
from wikipedia_scraper import WikipediaScraper


class DataProcessor:
    def __init__(self, sheet_handler, wiki_scraper):
        self.logger = logging.getLogger(__name__ + ".DataProcessor")
        self.logger.info("Initialisiere DataProcessor...")
        if not isinstance(sheet_handler, GoogleSheetHandler): raise ValueError("...")
        if not isinstance(wiki_scraper, WikipediaScraper): raise ValueError("...")
        self.sheet_handler = sheet_handler
        self.wiki_scraper = wiki_scraper
        self.model = None
        self.imputer = None
        self._expected_features = None
        self.is_setup_complete = False
        self.schema_data = None  # Wichtig: Neues Attribut

    def setup(self):
        self.logger.info("Führe DataProcessor-Setup durch...")
        # Lade das Branchenschema und speichere es in der Instanz
        self.schema_data = initialize_target_schema()
        if not self.schema_data:
            self.logger.error("Setup fehlgeschlagen: Branchenschema konnte nicht geladen werden.")
            self.is_setup_complete = False
            return False
        
        self._load_ml_model(MODEL_FILE, IMPUTER_FILE)
        self.is_setup_complete = True
        self.logger.info("DataProcessor-Setup erfolgreich abgeschlossen.")
        return True

    def _get_cell_value_safe(self, row, column_key):
        """
        Greift sicher auf eine Zelle in einer Zeile zu, basierend auf dem Spaltennamen.
        Angepasst an die neue COLUMN_MAP Struktur.
        """
        col_info = COLUMN_MAP.get(column_key)
        if col_info is None or 'index' not in col_info:
            self.logger.error(f"Spalte '{column_key}' oder ihr 'index' nicht im COLUMN_MAP gefunden.")
            return ""
        
        idx = col_info['index']
        if len(row) > idx:
            return row[idx]
        return ""

    def _needs_website_processing(self, row_data, force_reeval):
        """
        Prueft, ob Website-Scraping/Summarization fuer diese Zeile noetig ist.
        """
        if force_reeval:
            return True
        at_value = self._get_cell_value_safe(
            row_data, "Website Scrape Timestamp").strip()
        if not at_value:
            return True
        return False

    def _needs_wiki_processing(self, row_data, force_reeval):
        """
        Prueft, ob Wikipedia-Suche/Extraktion fuer diese Zeile noetig ist.
        """
        if force_reeval:
            return True
        an_value = self._get_cell_value_safe(
            row_data, "Wikipedia Timestamp").strip()
        if not an_value:
            return True
        s_value = self._get_cell_value_safe(
            row_data, "Chat Wiki Konsistenzpruefung").strip().upper()
        if s_value == "X (URL COPIED)":
            return True
        return False

    def _needs_wiki_verification(self, row_data, force_reeval):
        """
        Prueft, ob Wikipedia-Verifizierung fuer diese Zeile noetig ist.
        """
        if force_reeval:
            return True
        ax_value = self._get_cell_value_safe(
            row_data, "Wiki Verif. Timestamp").strip()
        if not ax_value:
            m_value = self._get_cell_value_safe(row_data, "Wiki URL").strip()
            if m_value and m_value.lower() not in [
                    "k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]:
                return True
        return False

    def _needs_chat_evaluations(
            self,
            row_data,
            force_reeval,
            wiki_data_just_updated):
        """
        Prueft, ob ChatGPT-Evaluationen fuer diese Zeile noetig sind.
        """
        if force_reeval:
            return True
        ao_value = self._get_cell_value_safe(
            row_data, "Timestamp letzte Pruefung").strip()
        if not ao_value:
            return True
        if wiki_data_just_updated:
            return True
        return False

    def _needs_ml_prediction(self, row_data, force_reeval, chat_eval_just_ran):
        """
        Prueft, ob die ML-Schaetzung fuer diese Zeile noetig ist.
        """
        if force_reeval:
            return True
        au_value = self._get_cell_value_safe(
            row_data, "Geschaetzter Techniker Bucket").strip()
        if not au_value or au_value.lower() in ["k.a.", "fehler schaetzung"]:
            if chat_eval_just_ran:
                return True
            av_value = self._get_cell_value_safe(
                row_data, "Finaler Umsatz (Wiki>CRM)").strip()
            aw_value = self._get_cell_value_safe(
                row_data, "Finaler Mitarbeiter (Wiki>CRM)").strip()
            if av_value != "k.A." and not av_value.startswith(
                    "FEHLER") and aw_value != "k.A." and not aw_value.startswith("FEHLER"):
                return True
        return False

    def _process_single_row(self, row_num_in_sheet, row_data, steps_to_run, force_reeval=False, clear_x_flag=False):
        """
        Verarbeitet die Daten fuer eine einzelne Zeile im Sheet. Fuehrt ausgewaehlte
        Anreicherungs- und Analyseprozesse durch.
        """
        self.logger.info(
            f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Angefragte Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine'}) ---")
        
        # NEU: Detaillierteres Logging der auszuführenden Schritte
        run_reasons = []
        if 'web' in steps_to_run and self._needs_website_processing(row_data, force_reeval):
            run_reasons.append("WEB (Timestamp leer oder Re-Eval)")
        if 'wiki' in steps_to_run and self._needs_wiki_processing(row_data, force_reeval):
            run_reasons.append("WIKI (Timestamp leer oder Re-Eval)")
        if 'chat' in steps_to_run and self._needs_chat_evaluations(row_data, force_reeval, False): # False, da wir es nur für die Logik prüfen
            run_reasons.append("CHAT (Timestamp leer oder Re-Eval)")
        if 'ml_predict' in steps_to_run and self._needs_ml_prediction(row_data, force_reeval, False):
            run_reasons.append("ML (Bucket leer oder Re-Eval)")
        
        if run_reasons:
            self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre Schritte aus -> {', '.join(run_reasons)}")
        
        updates = []
        now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        any_processing_done = False
        wiki_data_updated_in_this_run = False
        chat_eval_just_ran = False

        # --- Initiale Werte lesen ---
        company_name = self._get_cell_value_safe(row_data, "CRM Name").strip()
        website_url = self._get_cell_value_safe(
            row_data, "CRM Website").strip()
        crm_kurzform = self._get_cell_value_safe(
            row_data, "CRM Kurzform").strip()
        crm_branche = self._get_cell_value_safe(
            row_data, "CRM Branche").strip()
        crm_beschreibung = self._get_cell_value_safe(
            row_data, "CRM Beschreibung").strip()
        parent_account_name_d = self._get_cell_value_safe(
            row_data, "Parent Account Name").strip()

        current_wiki_data = {
            'url': self._get_cell_value_safe(row_data, "Wiki URL") or 'k.A.',
            'sitz_stadt': self._get_cell_value_safe(row_data, "Wiki Sitz Stadt") or 'k.A.',
            'sitz_land': self._get_cell_value_safe(row_data, "Wiki Sitz Land") or 'k.A.',
            'first_paragraph': self._get_cell_value_safe(row_data, "Wiki Absatz") or 'k.A.',
            'branche': self._get_cell_value_safe(row_data, "Wiki Branche") or 'k.A.',
            'umsatz': self._get_cell_value_safe(row_data, "Wiki Umsatz") or 'k.A.',
            'mitarbeiter': self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") or 'k.A.',
            'categories': self._get_cell_value_safe(row_data, "Wiki Kategorien") or 'k.A.'
        }
        final_wiki_data = current_wiki_data.copy()

        website_raw = self._get_cell_value_safe(
            row_data, "Website Rohtext") or 'k.A.'
        website_summary = self._get_cell_value_safe(
            row_data, "Website Zusammenfassung") or 'k.A.'
        website_meta_details = self._get_cell_value_safe(
            row_data, "Website Meta-Details") or 'k.A.'
        url_pruefstatus = self._get_cell_value_safe(
            row_data, "URL Prüfstatus") or ''

        run_website_step = 'web' in steps_to_run
        website_processing_needed = self._needs_website_processing(
            row_data, force_reeval)

        if run_website_step and website_processing_needed:
            any_processing_done = True
            grund_message = "Re-Eval" if force_reeval else "Timestamp (AJ) leer"
            self.logger.info(
                f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...")
            
            # WICHTIG: url_pruefstatus muss initialisiert werden, um Fehler zu vermeiden
            url_pruefstatus = self._get_cell_value_safe(row_data, "URL Prüfstatus")

            if not website_url or website_url.lower() == "k.a.":
                self.logger.debug(
                    "  -> Website URL (E) leer, suche ueber SERP...")
                try:
                    new_website = serp_website_lookup(company_name)
                    if new_website and new_website.lower(
                    ) != "k.a." and not new_website.startswith("k.A. (Fehler"):
                        website_url = new_website
                        updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("CRM Website") + 1)}{row_num_in_sheet}',
                                'values': [
                                    [website_url]]})
                        url_pruefstatus = "URL_OK_SERP"
                    else:
                        url_pruefstatus = "URL_SERP_FAILED" if not website_url or website_url.lower(
                        ) == "k.a." else url_pruefstatus
                except Exception as e_serp_lookup_web:
                    self.logger.error(
                        f"FEHLER bei SERP Website Lookup für '{company_name}': {e_serp_lookup_web}")
                    url_pruefstatus = "URL_SERP_ERROR"

            if website_url and website_url.lower() not in ["k.a.", "http:"]:
                self.logger.debug(
                    f"  -> Scrape Rohtext & Meta von {website_url[:100]}...")
                try:
                    website_raw = get_website_raw(website_url)
                    if website_raw == URL_CHECK_MARKER:
                        url_pruefstatus = URL_CHECK_MARKER
                        website_summary, website_meta_details = "k.A. (URL prüfen)", "k.A. (URL prüfen)"
                    elif website_raw and str(website_raw).strip().lower() not in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"]:
                        url_pruefstatus = "URL_OK_SCRAPED"
                        website_meta_details = scrape_website_details(
                            website_url) or "k.A. (Keine Meta-Details)"
                        # Verbessert: company_name für besseren Kontext übergeben
                        website_summary = summarize_website_content(
                            website_raw, company_name) or "k.A. (Keine Zusammenfassung erhalten)"
                        updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Website Meta-Details") + 1)}{row_num_in_sheet}',
                                'values': [
                                    [website_meta_details]]})
                        updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Website Zusammenfassung") + 1)}{row_num_in_sheet}',
                                'values': [
                                    [website_summary]]})
                    else:
                        if not str(website_raw).startswith("k.A. (Fehler"):
                            url_pruefstatus = "URL_SCRAPE_EMPTY_OR_BANNER"
                        website_summary, website_meta_details = "k.A.", "k.A."
                        updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Website Zusammenfassung") + 1)}{row_num_in_sheet}',
                                'values': [
                                    [website_summary]]})
                        updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Website Meta-Details") + 1)}{row_num_in_sheet}',
                                'values': [
                                    [website_meta_details]]})
                except Exception as e_scrape_web:
                    self.logger.error(
                        f"FEHLER beim Website Scraping für '{company_name}': {e_scrape_web}")
                    website_raw, website_summary, website_meta_details = f"k.A. (Fehler Scraping: {str(e_scrape_web)[:50]})", "k.A.", "k.A."
                    url_pruefstatus = "URL_SCRAPE_ERROR"
                    updates.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Website Zusammenfassung") + 1)}{row_num_in_sheet}',
                            'values': [
                                [website_summary]]})
                    updates.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Website Meta-Details") + 1)}{row_num_in_sheet}',
                            'values': [
                                [website_meta_details]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Website Rohtext") + 1)}{row_num_in_sheet}',
                        'values': [
                            [website_raw]]})
            else:
                self.logger.debug(
                    f"  -> Keine gültige Website URL für '{company_name}'. Web-Verarbeitung übersprungen.")
                website_raw, website_summary, website_meta_details = "k.A. (Keine URL)", "k.A.", "k.A."
                if not url_pruefstatus:
                    url_pruefstatus = "URL_MISSING"
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Website Rohtext") + 1)}{row_num_in_sheet}',
                        'values': [
                            [website_raw]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Website Zusammenfassung") + 1)}{row_num_in_sheet}',
                        'values': [
                            [website_summary]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Website Meta-Details") + 1)}{row_num_in_sheet}',
                        'values': [
                            [website_meta_details]]})

            updates.append(
                {
                    'range': f'{_get_col_letter(get_col_idx("URL Prüfstatus") + 1)}{row_num_in_sheet}',
                    'values': [
                        [url_pruefstatus]]})
            updates.append(
                {
                    'range': f'{_get_col_letter(get_col_idx("Website Scrape Timestamp") + 1)}{row_num_in_sheet}',
                    'values': [
                        [now_timestamp]]})

        # ======================================================================
        # === 2. Wikipedia Handling (Google-First Strategie) =================
        # ======================================================================
        run_wiki_step = 'wiki' in steps_to_run
        wiki_processing_needed = self._needs_wiki_processing(row_data, force_reeval)

        if run_wiki_step and wiki_processing_needed:
            any_processing_done = True
            wiki_data_updated_in_this_run = False # Wird auf True gesetzt, wenn neue Daten extrahiert werden
            self.logger.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia-Pipeline (Google-First)...")

            page_obj = None
            
            # Stufe 1: Prüfen, ob eine manuelle URL in Spalte R vorhanden und valide ist
            manual_url = self._get_cell_value_safe(row_data, "Wiki URL").strip()
            if manual_url and "wikipedia.org" in manual_url.lower():
                self.logger.debug(f"  -> Prüfe manuelle URL aus Spalte R: {manual_url}")
                try:
                    page_title = unquote(manual_url.split('/wiki/')[-1].replace('_', ' '))
                    page_candidate = wikipedia.page(title=page_title, auto_suggest=False, redirect=True)
                    # Die Validierung benötigt zusätzliche Daten aus der Zeile
                    crm_city = self._get_cell_value_safe(row_data, "CRM Ort")
                    parent_name_for_validation = self._get_cell_value_safe(row_data, "Parent Account Name")

                    if self.wiki_scraper._validate_article(page_candidate, company_name, website_url, crm_city, parent_name_for_validation):
                        self.logger.info(f"  -> Manuelle URL '{manual_url}' erfolgreich validiert.")
                        page_obj = page_candidate
                    else:
                        self.logger.warning(f"  -> Manuelle URL '{manual_url}' konnte nicht validiert werden. Starte Google-Suche als Fallback.")
                except Exception as e:
                    self.logger.error(f"  -> Fehler bei Verarbeitung der manuellen URL '{manual_url}': {e}")

            # Stufe 2: Wenn keine valide manuelle URL, starte die "Google-First" Suche
            if not page_obj:
                crm_city = self._get_cell_value_safe(row_data, "CRM Ort")
                parent_name_for_search = self._get_cell_value_safe(row_data, "Parent Account Name")
                page_obj = self.wiki_scraper.search_company_article(company_name, website_url, crm_city, parent_name_for_search)

            # Stufe 3: Ergebnisse verarbeiten und schreiben
            if page_obj:
                self.logger.debug(f"  -> Extrahiere Daten aus validiertem Artikel: '{page_obj.title}'")
                extracted_content = self.wiki_scraper.extract_company_data(page_obj)
                # Erstelle zusätzlich eine KI-Zusammenfassung des Rohtextes
                new_summary = summarize_wikipedia_article(extracted_content.get('full_text'), company_name)
                extracted_content['first_paragraph'] = new_summary
                
                final_wiki_data.update(extracted_content)
                wiki_data_updated_in_this_run = True
            else:
                self.logger.warning(f"  -> Keine passende & validierte URL gefunden. Setze Wiki-Daten auf 'Kein Artikel gefunden'.")
                no_article_data = {key: 'Kein Artikel gefunden' for key in final_wiki_data.keys()}
                no_article_data['url'] = 'Kein Artikel gefunden' # Sicherstellen, dass auch die URL zurückgesetzt wird
                final_wiki_data.update(no_article_data)
                wiki_data_updated_in_this_run = True

            # Schreibe die finalen Wiki-Daten (entweder extrahiert oder "Kein Artikel gefunden")
            key_mapping = {
                'Wiki URL': 'url', 'Wiki Sitz Stadt': 'sitz_stadt', 'Wiki Sitz Land': 'sitz_land',
                'Wiki Absatz': 'first_paragraph', 'Wiki Branche': 'branche', 'Wiki Umsatz': 'umsatz',
                'Wiki Mitarbeiter': 'mitarbeiter', 'Wiki Kategorien': 'categories'
            }
            for sheet_col_name, data_key in key_mapping.items():
                col_idx = get_col_idx(sheet_col_name)
                if col_idx is not None:
                    updates.append({'range': f'{_get_col_letter(col_idx + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get(data_key, 'k.A.')]]})
            
            # Setze immer den Timestamp, um zu zeigen, dass dieser Prozess gelaufen ist
            updates.append({'range': f'{_get_col_letter(get_col_idx("Wikipedia Timestamp") + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})

        # --- 3. ChatGPT Evaluationen (Branch, FSM, etc.) & Plausi ---
        run_chat_step = 'chat' in steps_to_run
        # KORREKTUR: Initialisiere chat_steps_to_run, um den NameError zu beheben
        chat_steps_to_run = set() 
        chat_processing_needed = self._needs_chat_evaluations(
            row_data, force_reeval, wiki_data_updated_in_this_run)

        if run_chat_step and chat_processing_needed:
            any_processing_done = True
            chat_eval_just_ran = True

            grund_message_chat = "Re-Eval" if force_reeval else (
                "Wiki-Daten aktualisiert" if wiki_data_updated_in_this_run else "Timestamp (BN) leer")
            self.logger.info(
                f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen & Plausi aus (Grund: {grund_message_chat})...")

        # --- 3. ChatGPT Evaluationen (Branch, FSM, etc.) & Plausi ---
        run_chat_step = 'chat' in steps_to_run
        chat_processing_needed = self._needs_chat_evaluations(
            row_data, force_reeval, wiki_data_updated_in_this_run)

        if run_chat_step and chat_processing_needed:
            any_processing_done = True
            chat_eval_just_ran = True

            grund_message_chat = "Re-Eval" if force_reeval else (
                "Wiki-Daten aktualisiert" if wiki_data_updated_in_this_run else "Timestamp (BN) leer")
            self.logger.info(
                f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen & Plausi aus (Grund: {grund_message_chat})...")

            # --- 3a. Branchen-Einstufung ---
            branch_result = {} # Initialisiere das Dictionary als leer
            self.logger.info(f"  Zeile {row_num_in_sheet}: Starte Branchen-Einstufung 2.0 ueber ChatGPT...")
            try:
                # Rufe die neue, kontextbasierte Funktion mit den korrekten Parametern auf
                branch_result = evaluate_branche_chatgpt(
                    company_name=company_name,
                    website_summary=website_summary,
                    wiki_absatz=final_wiki_data.get('first_paragraph', 'k.A.')
                )
                
                # Das Ergebnis von Version 2.0 ist bereits ein sauberes Dictionary
                updates.append({'range': f'{_get_col_letter(get_col_idx("Chat Vorschlag Branche") + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'FEHLER')]]})
                updates.append({'range': f'{_get_col_letter(get_col_idx("Chat Branche Konfidenz") + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('confidence', 'N/A')]]})
                updates.append({'range': f'{_get_col_letter(get_col_idx("Chat Konsistenz Branche") + 1)}{row_num_in_sheet}', 'values': [['V2.0']]}) # Markierung, dass V2.0 lief
                updates.append({'range': f'{_get_col_letter(get_col_idx("Chat Begruendung Abweichung Branche") + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'k.A.')]]})
            
            except Exception as e_branch_eval:
                self.logger.error(f"FEHLER bei Branchen-Einstufung für Zeile {row_num_in_sheet}: {e_branch_eval}", exc_info=True)
                error_updates = [
                    {"key": "Chat Vorschlag Branche", "value": "FEHLER_CALL"},
                    {"key": "Chat Branche Konfidenz", "value": "N/A"},
                    {"key": "Chat Begruendung Abweichung Branche", "value": str(e_branch_eval)[:100]}
                ]
                for item in error_updates:
                    col_idx = get_col_idx(item["key"])
                    if col_idx is not None:
                        updates.append({
                            'range': f'{_get_col_letter(col_idx + 1)}{row_num_in_sheet}',
                            'values': [[item["value"]]]
                        })
            # --- 3b. Parent-Account-Vorschlag (KI) ---
            # Prüfen, ob ein Vorschlag benötigt wird (Spalte Q ist leer)
            parent_timestamp = self._get_cell_value_safe(row_data, "Parent Vorschlag Timestamp").strip()
            if not parent_timestamp:
                self.logger.info(f"  Zeile {row_num_in_sheet}: Starte Parent-Account-Vorschlag über ChatGPT...")
                try:
                    # Sammle die Daten für die Task-Funktion
                    task_data = {
                        "row_num": row_num_in_sheet,
                        "crm_name": self._get_cell_value_safe(row_data, "CRM Name"),
                        "crm_website": self._get_cell_value_safe(row_data, "CRM Website"),
                        "crm_beschreibung": self._get_cell_value_safe(row_data, "CRM Beschreibung"),
                        "wiki_url": final_wiki_data.get('url', 'k.A.'),
                        "wiki_absatz": final_wiki_data.get('first_paragraph', 'k.A.'),
                        "wiki_kategorien": final_wiki_data.get('categories', 'k.A.'),
                        "website_summary": website_summary
                    }
                    
                    # Rufe die Logik der bestehenden Worker-Funktion auf
                    # Wir simulieren den Semaphore für den Einzelaufruf
                    temp_semaphore = threading.Semaphore(1)
                    parent_result = self._suggest_parent_account_openai_task(task_data, temp_semaphore)

                    parent_val = parent_result.get('suggested_parent', 'k.A. (Fehler)')
                    status_val = "?" if parent_val and parent_val.lower() != "k.a." and not parent_val.startswith("FEHLER") else ""
                    
                    updates.append({'range': f'{_get_col_letter(get_col_idx("System Vorschlag Parent Account") + 1)}{row_num_in_sheet}', 'values': [[parent_val]]})
                    updates.append({'range': f'{_get_col_letter(get_col_idx("Parent Vorschlag Status") + 1)}{row_num_in_sheet}', 'values': [[status_val]]})
                    updates.append({'range': f'{_get_col_letter(get_col_idx("Parent Vorschlag Timestamp") + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})

                except Exception as e_parent_sugg:
                    self.logger.error(f"FEHLER bei Parent-Account-Vorschlag für Zeile {row_num_in_sheet}: {e_parent_sugg}", exc_info=True)
                    updates.append({'range': f'{_get_col_letter(get_col_idx("System Vorschlag Parent Account") + 1)}{row_num_in_sheet}', 'values': [['FEHLER_CALL']]})




            # --- NEUER SCHRITT: FSM Pitch generieren ---
            if 'fsm_pitch' in chat_steps_to_run and self._needs_fsm_pitch(row_data, force_reeval):
                self.logger.info(f"  -> Generiere FSM Pitch...")
                try:
                    # Daten für den Pitch sammeln, die in diesem Durchlauf relevant sind
                    # Priorisiere die frisch generierte Branche, falle zurück auf Sheet-Daten
                    ki_branche = branch_result.get("branch") if branch_result else self._get_cell_value_safe(row_data, "Chat Vorschlag Branche")
                    if not ki_branche or "FEHLER" in ki_branche:
                        ki_branche = self._get_cell_value_safe(row_data, "CRM Branche")

                    # Rufe die neue, verbesserte Pitch-Funktion auf
                    fsm_pitch_text = generate_fsm_pitch(
                        company_name=self._get_cell_value_safe(row_data, "CRM Name"),
                        company_short_name=self._get_cell_value_safe(row_data, "CRM Kurzform"),
                        ki_branche=ki_branche,
                        website_summary=self._get_cell_value_safe(row_data, "Website Zusammenfassung"),
                        wiki_absatz=final_wiki_data.get('first_paragraph'), # Nutze die frischen Wiki-Daten
                        anzahl_ma=self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)"),
                        anzahl_techniker=self._get_cell_value_safe(row_data, "CRM Anzahl Techniker"),
                        techniker_bucket_ml=self._get_cell_value_safe(row_data, "Geschaetzter Techniker Bucket")
                    )
                    
                    updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch") + 1)}{row_num_in_sheet}', 'values': [[fsm_pitch_text]]})
                    updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch Timestamp") + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
                    any_processing_done = True
                    chat_eval_just_ran = True # Signal, dass eine KI-Aktion stattfand
                except Exception as e_fsm_pitch:
                     self.logger.error(f"FEHLER bei FSM-Pitch-Generierung für Zeile {row_num_in_sheet}: {e_fsm_pitch}")
                     updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch") + 1)}{row_num_in_sheet}', 'values': [['FEHLER (Generierung)']]} )



            # 3b, 3c, 3d: Weitere ChatGPT-Evaluationen (hier nicht detailliert implementiert, aber Platzhalter)
            # ... Logik für FSM-Relevanz, Mitarbeiter-Schätzung, Umsatz-Schätzung, etc. ...

            # 3e. Konsolidierung Umsatz/Mitarbeiter (BD, BE)
            self.logger.debug(f"  Zeile {row_num_in_sheet}: Konsolidiere Umsatz (BD) und Mitarbeiter (BE)...")
            final_umsatz_str_konsolidiert = "k.A."
            final_ma_str_konsolidiert = "k.A."
            try:
                crm_umsatz_val_str = self._get_cell_value_safe(
                    row_data, "CRM Umsatz")
                wiki_umsatz_val_str = final_wiki_data.get('umsatz', 'k.A.')
                crm_ma_val_str = self._get_cell_value_safe(
                    row_data, "CRM Anzahl Mitarbeiter")
                wiki_ma_val_str = final_wiki_data.get('mitarbeiter', 'k.A.')

                num_crm_umsatz = get_numeric_filter_value(
                    crm_umsatz_val_str, is_umsatz=True)
                num_wiki_umsatz = get_numeric_filter_value(
                    wiki_umsatz_val_str, is_umsatz=True)
                num_crm_ma = get_numeric_filter_value(
                    crm_ma_val_str, is_umsatz=False)
                num_wiki_ma = get_numeric_filter_value(
                    wiki_ma_val_str, is_umsatz=False)

                if parent_account_name_d and parent_account_name_d.lower() != 'k.a.':
                    self.logger.info(
                        f"    -> Parent D ('{parent_account_name_d}') gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.")
                    final_num_umsatz = num_crm_umsatz if num_crm_umsatz > 0 else num_wiki_umsatz
                    final_num_ma = num_crm_ma if num_crm_ma > 0 else num_wiki_ma
                else:
                    self.logger.debug(
                        f"    -> Parent D leer. Standardkonsolidierung Wiki > CRM.")
                    final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz
                    final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma

                final_umsatz_str_konsolidiert = str(
                    int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.'
                final_ma_str_konsolidiert = str(
                    int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.'
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Umsatz (Wiki>CRM)") + 1)}{row_num_in_sheet}',
                        'values': [
                            [final_umsatz_str_konsolidiert]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Mitarbeiter (Wiki>CRM)") + 1)}{row_num_in_sheet}',
                        'values': [
                            [final_ma_str_konsolidiert]]})
            except Exception as e_consolidate:
                self.logger.error(
                    f"FEHLER bei Konsolidierung (BD/BE) für Zeile {row_num_in_sheet}: {e_consolidate}")
                final_umsatz_str_konsolidiert, final_ma_str_konsolidiert = "FEHLER_KONSO", "FEHLER_KONSO"
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Umsatz (Wiki>CRM)") + 1)}{row_num_in_sheet}',
                        'values': [
                            ['FEHLER_KONSO']]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Mitarbeiter (Wiki>CRM)") + 1)}{row_num_in_sheet}',
                        'values': [
                            ['FEHLER_KONSO']]})

            # 3f. Plausibilitäts-Checks (BG-BM)
            self.logger.debug(
                f"  Zeile {row_num_in_sheet}: Führe Plausibilitäts-Checks durch...")
            try:
                plausi_input_data = {
                    "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert,
                    "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert,
                    "CRM Umsatz": self._get_cell_value_safe(
                        row_data,
                        "CRM Umsatz"),
                    "Wiki Umsatz": final_wiki_data.get(
                        'umsatz',
                        'k.A.'),
                    "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(
                        row_data,
                        "CRM Anzahl Mitarbeiter"),
                    "Wiki Mitarbeiter": final_wiki_data.get(
                        'mitarbeiter',
                        'k.A.'),
                    "Parent Account Name": parent_account_name_d,
                    "System Vorschlag Parent Account": self._get_cell_value_safe(
                        row_data,
                        "System Vorschlag Parent Account"),
                    "Parent Vorschlag Status": self._get_cell_value_safe(
                        row_data,
                        "Parent Vorschlag Status")}
                plausi_results = self._check_financial_plausibility(
                    plausi_input_data)
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Plausibilität Umsatz") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "plaus_umsatz_flag",
                                    "ERR_FLAG")]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Plausibilität Mitarbeiter") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "plaus_ma_flag",
                                    "ERR_FLAG")]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Plausibilität Umsatz/MA Ratio") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "plaus_ratio_flag",
                                    "ERR_FLAG")]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Abweichung Umsatz CRM/Wiki") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "abweichung_umsatz_flag",
                                    "ERR_FLAG")]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Abweichung MA CRM/Wiki") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "abweichung_ma_flag",
                                    "ERR_FLAG")]]})
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Plausibilität Begründung") + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                plausi_results.get(
                                    "plausi_begruendung_final",
                                    "Fehler Begr.")]]})
            except Exception as e_plausi:
                self.logger.error(
                    f"FEHLER bei Plausi-Checks für Zeile {row_num_in_sheet}: {e_plausi}")

            updates.append(
                {
                    'range': f'{_get_col_letter(get_col_idx("Plausibilität Prüfdatum") + 1)}{row_num_in_sheet}',
                    'values': [
                        [now_timestamp]]})
            updates.append(
                {
                    'range': f'{_get_col_letter(get_col_idx("Timestamp letzte Pruefung") + 1)}{row_num_in_sheet}',
                    'values': [
                        [now_timestamp]]})

        # --- 4. Servicetechniker Schaetzung (ML Modell) ---
        run_ml_step = 'ml_predict' in steps_to_run
        ml_processing_needed = self._needs_ml_prediction(
            row_data, force_reeval, chat_eval_just_ran)

        if run_ml_step and ml_processing_needed:
            any_processing_done = True
            self.logger.info(
                f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...")
            try:
                predicted_bucket = self._predict_technician_bucket(row_data)
                if predicted_bucket and isinstance(
                        predicted_bucket,
                        str) and not predicted_bucket.startswith("FEHLER"):
                    updates.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Geschaetzter Techniker Bucket") + 1)}{row_num_in_sheet}',
                            'values': [
                                [predicted_bucket]]})
                    self.logger.info(
                        f"  -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.")
                else:
                    self.logger.warning(
                        f"  -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.")
                    updates.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Geschaetzter Techniker Bucket") + 1)}{row_num_in_sheet}',
                            'values': [
                                ['k.A. (Schaetzung fehlgeschlagen)']]})
            except Exception as e_ml:
                self.logger.error(
                    f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}")
                self.logger.debug(traceback.format_exc())
                updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Geschaetzter Techniker Bucket") + 1)}{row_num_in_sheet}',
                        'values': [
                            [f'FEHLER Schaetzung: {str(e_ml)[:50]}...']]})

        # ======================================================================
        # === Abschluss der _process_single_row Verarbeitung ==================
        # ======================================================================

        # --- 5. Abschliessende Updates (Version, Tokens, ReEval Flag) ---
        if any_processing_done:
            version_col_idx = get_col_idx("Version") # KORRIGIERT
            if version_col_idx is not None:
                updates.append(
                    {
                        'range': f'{_get_col_letter(version_col_idx + 1)}{row_num_in_sheet}',
                        'values': [
                            [
                                getattr(
                                    Config,
                                    'VERSION',
                                    'unknown')]]})
            else:
                self.logger.error(
                    "FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.")

        if force_reeval and clear_x_flag:
            # KORRIGIERT: Nutze die sichere get_col_idx Funktion
            reeval_col_idx = get_col_idx("ReEval Flag")
            if reeval_col_idx is not None:
                flag_col_letter = _get_col_letter(
                    reeval_col_idx + 1)
                if flag_col_letter:
                    updates.append(
                        {'range': f'{flag_col_letter}{row_num_in_sheet}', 'values': [['']]})
                    self.logger.debug(
                        f"  -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.")
                else:
                    self.logger.error(
                        f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' nicht ermitteln.")
            else:
                self.logger.error(
                    "FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.")

        # --- 6. Batch Update fuer diese Zeile ---
        if updates:
            self.logger.info(
                f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...")
            success = self.sheet_handler.batch_update_cells(updates)
            if not success:
                self.logger.error(
                    f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.")
        else:
            if not any_processing_done:
                self.logger.debug(
                    f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.")

        pause_duration = max(0.05, getattr(Config, 'RETRY_DELAY', 5) / 20.0)
        time.sleep(pause_duration)
        self.logger.info(
            f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---")

        # ==========================================================================
        # === Prozess Methoden (Sequentiell & Re-Evaluation) =====================
        # ==========================================================================


    def process_website_scraping(self, start_sheet_row=None, end_sheet_row=None, limit=None):
        """
        Batch-Prozess NUR für Website-Scraping (Rohtext & Meta-Details).
        Diese Version ist stabilisiert, nutzt einen robusten Worker (_scrape_website_task_batch)
        und verarbeitet strukturierte Dictionary-Ergebnisse, inklusive des URL-Prüfstatus.
        """
        self.logger.info(f"Starte Website-Scraping (Batch, v2.0.7). Bereich: {start_sheet_row or 'Start'}-{end_sheet_row or 'Ende'}, Limit: {limit or 'Unbegrenzt'}")

        # --- 1. Daten laden und Startzeile ermitteln ---
        if start_sheet_row is None:
            self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren 'Website Scrape Timestamp'...")
            start_data_idx = self.sheet_handler.get_start_row_index(check_column_key="Website Scrape Timestamp")
            if start_data_idx == -1:
                self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
                return
            start_sheet_row = start_data_idx + self.sheet_handler._header_rows + 1
            self.logger.info(f"Automatisch ermittelte Startzeile: {start_sheet_row}")

        if not self.sheet_handler.load_data():
            self.logger.error("FEHLER beim Laden der Daten für Batch-Verarbeitung.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)
        effective_end_row = end_sheet_row if end_sheet_row is not None else total_sheet_rows
        
        self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {effective_end_row}.")
        if start_sheet_row > effective_end_row:
             self.logger.info("Start liegt nach dem Ende. Keine Zeilen zu verarbeiten.")
             return

        # --- 2. Spalten-Indizes und Buchstaben vorbereiten ---
        rohtext_col_letter = _get_col_letter(get_col_idx("Website Rohtext") + 1)
        metadetails_col_letter = _get_col_letter(get_col_idx("Website Meta-Details") + 1)
        pruefstatus_col_letter = _get_col_letter(get_col_idx("URL Prüfstatus") + 1) # NEU
        version_col_letter = _get_col_letter(get_col_idx("Version") + 1)
        timestamp_col_letter = _get_col_letter(get_col_idx("Website Scrape Timestamp") + 1)

        # --- 3. Tasks sammeln ---
        processing_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20)
        max_scraping_workers = getattr(Config, 'MAX_SCRAPING_WORKERS', 10)
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        tasks_for_processing_batch = []
        all_sheet_updates = []
        processed_count = 0
        skipped_count = 0

        for i in range(start_sheet_row, effective_end_row + 1):
            row_index_in_list = i - 1
            if row_index_in_list >= total_sheet_rows: break
            
            row = all_data[row_index_in_list]
            if not any(cell and str(cell).strip() for cell in row):
                 skipped_count += 1
                 continue

            if self._needs_website_processing(row, force_reeval=False):
                website_url = self._get_cell_value_safe(row, "CRM Website").strip()
                if website_url and website_url.lower() not in ["k.a.", "http:"]:
                    if limit is not None and processed_count >= limit:
                        self.logger.info(f"Verarbeitungslimit ({limit}) erreicht.")
                        break
                    
                    tasks_for_processing_batch.append({"row_num": i, "url": website_url})
                    processed_count += 1
                else:
                    skipped_count += 1
            else:
                skipped_count += 1

            # --- 4. Batch-Verarbeitung auslösen ---
            if len(tasks_for_processing_batch) >= processing_batch_size or (i == effective_end_row and tasks_for_processing_batch):
                self.logger.info(f"--- Starte Website-Scraping Batch für {len(tasks_for_processing_batch)} Tasks (max. {max_scraping_workers} Worker) ---")
                
                scraping_results = {}
                batch_error_count = 0
                
                with ThreadPoolExecutor(max_workers=max_scraping_workers) as executor:
                    future_to_task = {executor.submit(self._scrape_website_task_batch, task): task for task in tasks_for_processing_batch}
                    
                    for future in as_completed(future_to_task):
                        task = future_to_task[future]
                        try:
                            result_dict = future.result()
                            if isinstance(result_dict, dict) and 'row_num' in result_dict:
                                scraping_results[result_dict['row_num']] = result_dict
                                if result_dict.get('error'):
                                    batch_error_count += 1
                                    self.logger.warning(f"Worker meldete Fehler für Zeile {result_dict['row_num']}: {result_dict.get('status_message')}")
                            else:
                                self.logger.error(f"Inkonsistentes Ergebnis für Zeile {task['row_num']}: Erwartete dict mit 'row_num', bekam {type(result_dict)}. Überspringe.")
                                scraping_results[task['row_num']] = {'raw_text': "FEHLER (Inkonsistenter Rückgabetyp)", 'meta_details': 'k.A.', 'url_pruefstatus': 'URL_SCRAPE_ERROR', 'error': True}
                                batch_error_count += 1
                        except Exception as exc:
                            self.logger.error(f"Unerwarteter Fehler bei Ergebnisabfrage für Zeile {task['row_num']}: {exc}", exc_info=True)
                            scraping_results[task['row_num']] = {'raw_text': "FEHLER (Task Exception)", 'meta_details': 'k.A.', 'url_pruefstatus': 'URL_SCRAPE_ERROR', 'error': True}
                            batch_error_count += 1
                
                self.logger.info(f"  -> Scraping für Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} davon mit Fehlern).")

                # --- 5. Updates für das Google Sheet vorbereiten ---
                if scraping_results:
                    current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    current_version = getattr(Config, 'VERSION', 'unknown')
                    
                    for row_num, res_dict in scraping_results.items():
                        all_sheet_updates.append({'range': f'{rohtext_col_letter}{row_num}', 'values': [[res_dict.get('raw_text', 'k.A.')]]})
                        all_sheet_updates.append({'range': f'{metadetails_col_letter}{row_num}', 'values': [[res_dict.get('meta_details', 'k.A.')]]})
                        all_sheet_updates.append({'range': f'{pruefstatus_col_letter}{row_num}', 'values': [[res_dict.get('url_pruefstatus', 'URL_SCRAPE_ERROR')]]}) # NEU
                        all_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [[current_timestamp]]})
                        all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]})

                tasks_for_processing_batch = []

                # --- 6. Sheet-Update auslösen, wenn Update-Batch voll ist ---
                if len(all_sheet_updates) >= (update_batch_row_limit * 5): # NEU: 5 Updates pro Zeile
                    self.logger.info(f"Sende gesammelte Sheet-Updates ({len(all_sheet_updates) // 5} Zeilen)...")
                    self.sheet_handler.batch_update_cells(all_sheet_updates)
                    all_sheet_updates = []
                    time.sleep(1)

        # --- 7. Finale Updates senden ---
        if all_sheet_updates:
            self.logger.info(f"Sende finale gesammelte Sheet-Updates ({len(all_sheet_updates) // 5} Zeilen)...")
            self.sheet_handler.batch_update_cells(all_sheet_updates)

        self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen zur Verarbeitung ausgewählt, {skipped_count} Zeilen übersprungen.")

    def _scrape_website_task_batch(self, task_info):
        """
        Robuste Worker-Funktion für das parallele Scrapen von Websites im Batch-Modus.
        Diese Funktion holt Rohtext sowie Meta-Details und gibt IMMER ein strukturiertes
        Dictionary zurück, das auch den programmatischen URL-Prüfstatus enthält.
        NEU: Enthält Logik zur Erkennung von "Thin Content" und Cookie-Bannern.
        """
        row_num = task_info['row_num']
        url = task_info['url']
        self.logger.debug(f"  -> Batch-Scrape-Task gestartet für Zeile {row_num}: {url}")

        result = {
            'row_num': row_num,
            'raw_text': 'k.A. (Fehler im Task)',
            'meta_details': 'k.A. (Fehler im Task)',
            'error': True,
            'status_message': 'Unbekannter Task-Fehler',
            'url_pruefstatus': 'URL_SCRAPE_ERROR' # Default-Fehlerstatus
        }

        try:
            # 1. Rohtext abrufen (get_website_raw aus helpers.py)
            raw_text_result = get_website_raw(url)

            # 2. Ergebnis des Rohtext-Abrufs auswerten
            if raw_text_result == URL_CHECK_MARKER:
                result.update({
                    'raw_text': "k.A. (URL prüfen)", 'meta_details': "k.A.", 'error': True,
                    'status_message': "URL als nicht erreichbar markiert", 'url_pruefstatus': URL_CHECK_MARKER
                })
            
            elif raw_text_result and not str(raw_text_result).strip().lower().startswith('k.a.'):
                # --- NEUER BLOCK: Thin Content & Cookie-Banner-Erkennung ---
                text_len = len(raw_text_result)
                text_lower_sample = raw_text_result[:600].lower() # Prüfe nur den Anfang
                cookie_keywords = ['cookie', 'zustimmen', 'akzeptieren', 'einwilligung', 'datenschutz', 'ablehnen', 'einstellungen']
                keyword_hits = sum(1 for keyword in cookie_keywords if keyword in text_lower_sample)

                if text_len < 500 and keyword_hits >= 3:
                    self.logger.warning(f"Zeile {row_num}: Potenzieller Cookie-Banner erkannt (Länge: {text_len}, Keyword-Treffer: {keyword_hits}).")
                    result.update({
                        'raw_text': raw_text_result, 'meta_details': "k.A.", 'error': True,
                        'status_message': "Nur Cookie-Banner erkannt", 'url_pruefstatus': 'URL_SCRAPE_COOKIE_BANNER'
                    })
                elif text_len < 200:
                    self.logger.warning(f"Zeile {row_num}: 'Thin Content' erkannt (Länge: {text_len}).")
                    result.update({
                        'raw_text': raw_text_result, 'meta_details': "k.A.", 'error': True,
                        'status_message': "Inhalt zu kurz", 'url_pruefstatus': 'URL_SCRAPE_THIN_CONTENT'
                    })
                else:
                    # Dies ist der reguläre Erfolgsfall
                    meta_details_result = scrape_website_details(url)
                    result.update({
                        'raw_text': raw_text_result, 'meta_details': meta_details_result if meta_details_result else "k.A.",
                        'error': False, 'status_message': 'Erfolgreich gescraped', 'url_pruefstatus': 'URL_OK_SCRAPED'
                    })
                # --- ENDE NEUER BLOCK ---

            elif str(raw_text_result).strip().lower().startswith('k.a.'):
                result['raw_text'] = raw_text_result # Fehlerstring übernehmen
                result['meta_details'] = "k.A."
                result['error'] = True
                match = re.search(r'\((.*?)\)', raw_text_result)
                result['status_message'] = match.group(1) if match else "Scraping fehlgeschlagen"
                result['url_pruefstatus'] = "URL_SCRAPE_ERROR"
            
            else: # Fallback für unerwartete leere Ergebnisse
                result.update({
                    'raw_text': 'k.A. (Extraktion leer)', 'meta_details': 'k.A.', 'error': True,
                    'status_message': 'Extraktion lieferte leeren Text', 'url_pruefstatus': "URL_SCRAPE_EMPTY_OR_BANNER"
                })

            return result

        except Exception as e:
            self.logger.error(f"  -> Kritischer Fehler im Worker-Task `_scrape_website_task_batch` für Zeile {row_num}: {e}")
            # Das `result` Dictionary wird mit den initialen Fehlerwerten zurückgegeben.
            return result


    def _summarize_task_batch(self, task_info):
        """
        Robuste Worker-Funktion für die parallele Website-Zusammenfassung.
        Wird vom ThreadPoolExecutor in `process_summarize_website` aufgerufen.
        Gibt IMMER ein strukturiertes Dictionary zurück.
        """
        row_num = task_info['row_num']
        raw_text = task_info['raw_text']
        company_name = task_info.get('company_name', 'einem Unternehmen')
        self.logger.debug(f"  -> Batch-Summarize-Task gestartet für Zeile {row_num}...")

        result = {
            'row_num': row_num,
            'summary': 'k.A. (Fehler im Task)',
            'error': True,
            'status_message': 'Unbekannter Task-Fehler'
        }

        try:
            # Ruft die gehärtete Single-Item-Funktion aus helpers.py auf
            summary_text = summarize_website_content(raw_text, company_name)

            if summary_text and not summary_text.lower().startswith('k.a.'):
                result['summary'] = summary_text
                result['error'] = False
                result['status_message'] = 'Erfolgreich zusammengefasst'
            else:
                result['summary'] = summary_text # Fehlergrund übernehmen
                result['error'] = True
                result['status_message'] = 'Zusammenfassung fehlgeschlagen oder Text zu kurz'
            
            return result

        except Exception as e:
            self.logger.error(f"  -> Kritischer Fehler im Worker-Task `_summarize_task_batch` für Zeile {row_num}: {e}")
            result['summary'] = f"FEHLER (API-Fehler bei Zusammenfassung)"
            result['status_message'] = f"Kritischer Task-Fehler: {type(e).__name__}"
            return result

    def process_rows_sequentially(
            self,
            start_sheet_row,
            num_to_process,
            process_wiki_steps=True,
            process_chatgpt_steps=True,
            process_website_steps=True,
            process_ml_steps=True,
            force_reeval_in_single_row=False):
        """
        Verarbeitet eine feste Anzahl von Zeilen beginnend bei einer bestimmten
        Sheet-Zeilennummer sequentiell.
        """
        header_rows = self.sheet_handler._header_rows
        if num_to_process is None or not isinstance(
                num_to_process, int) or num_to_process <= 0:
            self.logger.info(
                "Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.")
            return

        self.logger.info(
            f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...")
        selected_steps_log = []
        if process_wiki_steps:
            selected_steps_log.append("Wiki (wiki)")
        if process_chatgpt_steps:
            selected_steps_log.append("ChatGPT (chat)")
        if process_website_steps:
            selected_steps_log.append("Website (web)")
        if process_ml_steps:
            selected_steps_log.append("ML Predict (ml_predict)")
        self.logger.info(
            f"  Ausgewaehlte Schritte: {', '.join(selected_steps_log) if selected_steps_log else 'Keine'}")

        if force_reeval_in_single_row:
            self.logger.warning(
                "  !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!")

        steps_to_run_set = set()
        if process_wiki_steps:
            steps_to_run_set.add('wiki')
        if process_chatgpt_steps:
            steps_to_run_set.add('chat')
        if process_website_steps:
            steps_to_run_set.add('web')
        if process_ml_steps:
            steps_to_run_set.add('ml_predict')
        if not steps_to_run_set:
            self.logger.warning(
                "Keine Verarbeitungsschritte ausgewaehlt. Modus wird uebersprungen.")
            return

        if not self.sheet_handler.load_data():
            self.logger.error(
                "Fehler beim Laden der Daten fuer sequentielle Verarbeitung.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        total_sheet_rows = len(all_data)
        start_index_in_all_data = start_sheet_row - 1

        if start_index_in_all_data >= total_sheet_rows:
            self.logger.warning(
                f"Start-Sheet-Zeile {start_sheet_row} liegt ausserhalb der Daten. Keine Verarbeitung.")
            return
        if start_index_in_all_data < header_rows:
            self.logger.warning(
                f"Start-Sheet-Zeile {start_sheet_row} liegt in Headern. Starte ab Zeile {header_rows + 1}.")
            start_index_in_all_data = header_rows

        end_index_in_all_data = min(
            start_index_in_all_data +
            num_to_process,
            total_sheet_rows)
        self.logger.info(
            f"Sequentielle Verarbeitung: Index-Bereich [{start_index_in_all_data}, {end_index_in_all_data}).")

        if start_index_in_all_data >= end_index_in_all_data:
            self.logger.info(
                f"Keine Zeilen im definierten Bereich zu verarbeiten.")
            return

        processed_count = 0
        for i in range(start_index_in_all_data, end_index_in_all_data):
            row_num_in_sheet = i + 1
            row_data = all_data[i]
            if row_num_in_sheet <= header_rows:
                continue
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row_data):
                self.logger.debug(
                    f"Ueberspringe leere Zeile {row_num_in_sheet}.")
                continue
            try:
                self._process_single_row(
                    row_num_in_sheet=row_num_in_sheet,
                    row_data=row_data,
                    steps_to_run=steps_to_run_set,
                    force_reeval=force_reeval_in_single_row,
                    clear_x_flag=False
                )
                processed_count += 1
            except Exception as e_proc:
                self.logger.exception(
                    f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}")

        self.logger.info(
            f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen bearbeitet.")

    def process_reevaluation_rows(
            self,
            row_limit=None,
            clear_flag=True,
            process_wiki_steps=True,
            process_chatgpt_steps=True,
            process_website_steps=True,
            process_ml_steps=True):
        """
        Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
        Leert zuerst alle abgeleiteten Spalten für eine saubere Neubewertung.
        """
        self.logger.info(
            f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}")
        
        if not self.sheet_handler.load_data():
            self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        if not all_data or len(all_data) <= header_rows:
            self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.")
            return
        
        column_names = list(COLUMN_MAP.keys())
        try:
            reeval_col_idx = column_names.index("ReEval Flag")
        except ValueError:
            self.logger.critical("FEHLER: 'ReEval Flag' nicht in COLUMN_MAP. Breche ab.")
            return

        rows_to_process = []
        for idx, row_data in enumerate(all_data):
            if idx < header_rows: continue
            if self._get_cell_value_safe(row_data, "ReEval Flag").strip().lower() == "x":
                rows_to_process.append({'row_num': idx + 1, 'data': row_data})

        found_count = len(rows_to_process)
        self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.")
        if found_count == 0: return

        # Spalten definieren, die vor der Neubewertung geleert werden sollen
        cols_to_clear_keys = [
            "Wiki URL", "Wiki Sitz Stadt", "Wiki Sitz Land", "Wiki Absatz", "Wiki Branche",
            "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Wikipedia Timestamp",
            "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp", "Chat Wiki Konsistenzpruefung",
            "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung",
            "Website Rohtext", "Website Zusammenfassung", "Website Meta-Details", "Website Scrape Timestamp", "URL Prüfstatus",
            "Chat Vorschlag Branche", "Chat Branche Konfidenz", "Chat Konsistenz Branche",
            "Chat Begruendung Abweichung Branche", "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)",
            "Geschaetzter Techniker Bucket", "Plausibilität Umsatz", "Plausibilität Mitarbeiter",
            "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki",
            "Plausibilität Begründung", "Plausibilität Prüfdatum", "Timestamp letzte Pruefung", "Version", "Tokens",
            "FSM Pitch", "FSM Pitch Timestamp"
        ]
        
        clear_updates = []
        for task in rows_to_process:
            row_num = task['row_num']
            for key in cols_to_clear_keys:
                try:
                    col_idx = column_names.index(key)
                    col_letter = _get_col_letter(col_idx + 1)
                    clear_updates.append({'range': f'{col_letter}{row_num}', 'values': [['']]})
                except ValueError:
                    self.logger.warning(f"Spalte '{key}' zum Leeren im Re-Eval-Modus nicht in COLUMN_MAP gefunden. Überspringe.")
        
        if clear_updates:
            self.logger.info(f"Leere {len(clear_updates)} Zellen für {found_count} Re-Eval-Zeilen zur Vorbereitung...")
            self.sheet_handler.batch_update_cells(clear_updates)
            self.logger.info("Vorbereitung abgeschlossen. Starte eigentliche Verarbeitung...")
            time.sleep(2) 
            self.sheet_handler.load_data()

        processed_count_actual = 0
        steps_to_run_set = set(key for key, value in {'wiki': process_wiki_steps, 'chat': process_chatgpt_steps, 'web': process_website_steps, 'ml_predict': process_ml_steps}.items() if value)
        
        fresh_data = self.sheet_handler.get_all_data_with_headers()

        for task in rows_to_process:
            if row_limit is not None and processed_count_actual >= row_limit:
                self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht.")
                break

            current_row_data = fresh_data[task['row_num'] - 1]
            self.logger.info(f"Bearbeite Re-Eval Zeile {task['row_num']}...")
            processed_count_actual += 1
            try:
                self._process_single_row(
                    row_num_in_sheet=task['row_num'],
                    row_data=current_row_data,
                    steps_to_run=steps_to_run_set,
                    force_reeval=True,
                    clear_x_flag=clear_flag
                )
            except Exception as e_proc_reval:
                self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {task['row_num']}: {e_proc_reval}")

        self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count_actual} Zeilen verarbeitet.")



    def process_wiki_verify(self, limit=None, start_sheet_row=None, end_sheet_row=None):
        """
        Iteriert durch die Zeilen und führt eine ChatGPT-basierte Verifizierung des
        in Spalte R ("Wiki URL") gefundenen Artikels durch.
        Validiert von der KI vorgeschlagene URLs auf Existenz.
        """
        BATCH_SIZE = 20
        self.logger.info(f"Starte Modus: Wiki-Verifizierung via ChatGPT (Batch-Größe: {BATCH_SIZE})...")

        if not self.sheet_handler.load_data():
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        
        start_row_idx = (start_sheet_row - 1) if start_sheet_row is not None else header_rows
        end_row_idx = (end_sheet_row - 1) if end_sheet_row is not None else len(all_data) - 1
        
        rows_to_process = all_data[start_row_idx : end_row_idx + 1]
        
        processed_count_in_batch = 0
        total_processed_since_start = 0
        all_updates = []

        for idx, row_data in enumerate(rows_to_process):
            current_row_num = start_row_idx + idx + 1
            
            if limit is not None and total_processed_since_start >= limit:
                self.logger.info(f"Globales Limit von {limit} Zeilen erreicht. Stoppe.")
                break

            verif_timestamp = self._get_cell_value_safe(row_data, "Wiki Verif. Timestamp").strip()
            wiki_url = self._get_cell_value_safe(row_data, "Wiki URL").strip()
            
            if not verif_timestamp and wiki_url and "wikipedia.org" in wiki_url.lower():
                self.logger.info(f"Zeile {current_row_num}: Verifizierung nötig, füge zur Queue hinzu.")
                
                try:
                    # --- START KORREKTURBLOCK ---
                    # 1. Sammle alle notwendigen Daten für den neuen, kontextreichen Aufruf
                    company_name = self._get_cell_value_safe(row_data, "CRM Name")
                    website = self._get_cell_value_safe(row_data, "CRM Website")
                    parent_name = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account")
                    
                    # 2. Lade den Artikel-Kontext
                    self.logger.debug(f"  Lade Kontext für Artikel: {wiki_url}")
                    page_content = self.wiki_scraper.extract_company_data(wiki_url)
                    if not page_content or 'FEHLER' in str(page_content.get('title')):
                        self.logger.error(f"  Konnte Kontext für URL {wiki_url} nicht laden. Überspringe KI-Verifizierung für diese Zeile.")
                        continue # Gehe zur nächsten Zeile

                    # 3. Rufe die neue, korrekte Funktion auf
                    verification_result = verify_wiki_article_chatgpt(
                        company_name=company_name,
                        parent_name=parent_name,
                        website=website,
                        wiki_title=page_content.get('title', 'k.A.'),
                        wiki_summary=page_content.get('first_paragraph', 'k.A.')
                    )
                    # --- ENDE KORREKTURBLOCK ---

                    # Ihre bestehende Logik zur Validierung der vorgeschlagenen URL bleibt erhalten
                    final_suggested_url = verification_result.get("suggestion", "") # Geändert von "suggested_url"
                    if final_suggested_url and "wikipedia.org" in final_suggested_url.lower():
                        if not is_valid_wikipedia_article_url(final_suggested_url):
                            self.logger.warning(f"  -> KI-Vorschlag '{final_suggested_url}' ist eine ungültige URL.")
                            final_suggested_url = f"Vorschlag (ungültig): {final_suggested_url}"
                        else:
                             self.logger.info(f"  -> KI-Vorschlag '{final_suggested_url}' ist eine gültige URL.")

                    now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    
                    # Ihre bestehende Update-Logik, aber mit der sicheren get_col_idx Funktion
                    updates_for_row = [
                        {'range': f'{_get_col_letter(get_col_idx("Chat Wiki Konsistenzpruefung") + 1)}{current_row_num}', 'values': [[verification_result.get("consistency")]]},
                        {'range': f'{_get_col_letter(get_col_idx("Chat Begründung Wiki Inkonsistenz") + 1)}{current_row_num}', 'values': [[verification_result.get("justification")]]},
                        {'range': f'{_get_col_letter(get_col_idx("Chat Vorschlag Wiki Artikel") + 1)}{current_row_num}', 'values': [[final_suggested_url]]},
                        {'range': f'{_get_col_letter(get_col_idx("Wiki Verif. Timestamp") + 1)}{current_row_num}', 'values': [[now_timestamp]]}
                    ]
                    all_updates.extend(updates_for_row)
                    
                    processed_count_in_batch += 1
                    total_processed_since_start += 1
                    
                    if processed_count_in_batch >= BATCH_SIZE:
                        self.logger.info(f"Batch-Größe ({BATCH_SIZE}) erreicht. Schreibe {len(all_updates)} Zell-Updates...")
                        self.sheet_handler.batch_update_cells(all_updates)
                        self.logger.info("Batch erfolgreich geschrieben.")
                        all_updates = []
                        processed_count_in_batch = 0
                    
                    time.sleep(1)

                except Exception as e:
                    self.logger.error(f"Unerwarteter Fehler bei Verarbeitung von Zeile {current_row_num}: {e}", exc_info=True)
            else:
                self.logger.debug(f"Zeile {current_row_num}: Übersprungen (Timestamp vorhanden oder keine URL).")
        
        if all_updates:
            self.logger.info(f"Schleife beendet. Schreibe die letzten {len(all_updates)} Zell-Updates...")
            self.sheet_handler.batch_update_cells(all_updates)
            self.logger.info("Finaler Batch erfolgreich geschrieben.")

        self.logger.info(f"Wiki-Verifizierung abgeschlossen. {total_processed_since_start} Zeilen insgesamt verarbeitet.")


    def process_fsm_pitch(self, start_sheet_row=None, end_sheet_row=None, limit=None):
        """
        Generiert FSM-Pitches für alle Zeilen, bei denen der Pitch oder der Timestamp fehlt.
        Nutzt die neue, verbesserte generate_fsm_pitch Funktion.
        """
        self.logger.info(f"Starte Batch-Modus 'fsm_pitch'. Bereich: {start_sheet_row or 'Start'}-{end_sheet_row or 'Ende'}, Limit: {limit or 'Unbegrenzt'}")

        if not self.sheet_handler.load_data():
            self.logger.error("Fehler beim Laden der Daten für FSM-Pitch-Generierung.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        
        effective_start = start_sheet_row if start_sheet_row is not None else header_rows + 1
        effective_end = end_sheet_row if end_sheet_row is not None else len(all_data)

        tasks = []
        for i in range(effective_start - 1, effective_end):
            if limit is not None and len(tasks) >= limit:
                break

            row_data = all_data[i]
            timestamp = self._get_cell_value_safe(row_data, "FSM Pitch Timestamp").strip()
            pitch = self._get_cell_value_safe(row_data, "FSM Pitch").strip()
            
            if not timestamp or "fehler" in pitch.lower():
                company_name = self._get_cell_value_safe(row_data, "CRM Name").strip()
                if company_name:
                    tasks.append({'row_num': i + 1, 'data': row_data})

        if not tasks:
            self.logger.info("Keine Zeilen gefunden, die eine FSM-Pitch-Generierung erfordern.")
            return

        self.logger.info(f"{len(tasks)} Zeilen für FSM-Pitch-Generierung identifiziert. Starte Verarbeitung...")
        
        all_sheet_updates = []
        processed_count = 0
        update_batch_size = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)
        now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        for idx, task in enumerate(tasks):
            row_num = task['row_num']
            row_data = task['data']
            
            self.logger.debug(f"Verarbeite FSM-Pitch für Zeile {row_num}...")
            
            try:
                # Rufe die neue, verbesserte Pitch-Funktion mit Daten aus dem Sheet auf
                fsm_pitch_text = generate_fsm_pitch(
                    company_name=self._get_cell_value_safe(row_data, "CRM Name"),
                    company_short_name=self._get_cell_value_safe(row_data, "CRM Kurzform"),
                    ki_branche=self._get_cell_value_safe(row_data, "Chat Vorschlag Branche"),
                    website_summary=self._get_cell_value_safe(row_data, "Website Zusammenfassung"),
                    wiki_absatz=self._get_cell_value_safe(row_data, "Wiki Absatz"),
                    anzahl_ma=self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)"),
                    anzahl_techniker=self._get_cell_value_safe(row_data, "CRM Anzahl Techniker"),
                    techniker_bucket_ml=self._get_cell_value_safe(row_data, "Geschaetzter Techniker Bucket")
                )
                
                all_sheet_updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch") + 1)}{row_num}', 'values': [[fsm_pitch_text]]})
            except Exception as e:
                self.logger.error(f"FEHLER bei FSM-Pitch-Generierung für Zeile {row_num}: {e}")
                all_sheet_updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch") + 1)}{row_num}', 'values': [['FEHLER (Generierung)']]} )

            # IMMER den Timestamp setzen, auch bei Fehler, um Endlosschleifen zu vermeiden
            all_sheet_updates.append({'range': f'{_get_col_letter(get_col_idx("FSM Pitch Timestamp") + 1)}{row_num}', 'values': [[now_timestamp]]})
            processed_count += 1

            # Batch-Update Logik
            if (idx + 1) % update_batch_size == 0 or (idx + 1) == len(tasks):
                if all_sheet_updates:
                    num_rows_in_batch = len(all_sheet_updates) // 2
                    self.logger.info(f"Sende Batch-Update für {num_rows_in_batch} FSM-Pitches (Verarbeitet bisher: {processed_count}/{len(tasks)})...")
                    self.sheet_handler.batch_update_cells(all_sheet_updates)
                    all_sheet_updates = []
                    time.sleep(1)

        self.logger.info(f"FSM-Pitch-Generierung abgeschlossen. {processed_count} Zeilen bearbeitet.")

    def reclassify_all_branches(self, start_sheet_row=None, limit=None, batch_size=50):
        """
        Führt für alle relevanten Zeilen eine neue Brancheneinstufung (v2.0) in Batches durch.
        Nutzt nun auch die externe Branchenbeschreibung.
        """
        self.logger.info(f"Starte Modus 'reclassify_branches' im Batch-Modus (Größe: {batch_size}). Bereich: {start_sheet_row or 'Start'}, Limit: {limit or 'Unbegrenzt'}")

        if not self.sheet_handler.load_data():
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        
        # Definiere den Startpunkt. Wenn kein start_sheet_row übergeben wird, starte nach dem Header.
        effective_start = start_sheet_row if start_sheet_row is not None else header_rows + 1
        
        tasks = []
        # Die Schleife beginnt beim korrekten Startpunkt
        for i in range(effective_start - 1, len(all_data)):
            # Das Limit wird HIER geprüft, nicht vorher
            if limit is not None and len(tasks) >= limit:
                self.logger.info(f"Limit von {limit} Zeilen erreicht. Stoppe das Sammeln von Tasks.")
                break
            
            row_data = all_data[i]
            company_name = self._get_cell_value_safe(row_data, "CRM Name").strip()
            # Der Check auf "firmennamen" ist gut und bleibt
            if company_name and "firmennamen" not in company_name.lower():
                tasks.append({'row_num': i + 1, 'data': row_data})
        
        if not tasks:
            self.logger.info("Keine Zeilen zur Neubewertung gefunden.")
            return
        
        self.logger.info(f"{len(tasks)} Zeilen für die Neubewertung der Branche identifiziert.")
        
        all_sheet_updates = []
        now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        for i in range(0, len(tasks), batch_size):
            batch_tasks = tasks[i:i + batch_size]
            self.logger.info(f"Verarbeite Batch {i//batch_size + 1}/{(len(tasks) + batch_size - 1)//batch_size} (Zeilen {batch_tasks[0]['row_num']} bis {batch_tasks[-1]['row_num']})...")

            companies_data_for_prompt = []
            for task in batch_tasks:
                row_data = task['data']
                companies_data_for_prompt.append({
                    "row_num": task['row_num'],
                    "name": self._get_cell_value_safe(row_data, "CRM Name"),
                    # NEU: Spalte J hinzufügen
                    "external_branch_desc": self._get_cell_value_safe(row_data, "CRM Beschreibung Branche extern"),
                    "summary": self._get_cell_value_safe(row_data, "Website Zusammenfassung"),
                    "wiki": self._get_cell_value_safe(row_data, "Wiki Absatz")
                })
            
            batch_results = evaluate_branches_batch(companies_data_for_prompt)
            
            # ... (Rest der Funktion zum Verarbeiten der Ergebnisse und Schreiben der Updates bleibt unverändert) ...
            if batch_results:
                results_by_row = {res['row_num']: res for res in batch_results}
                for task in batch_tasks:
                    row_num = task['row_num']
                    result = results_by_row.get(row_num)
                    if result:
                        all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"]["index"] + 1)}{row_num}', 'values': [[result.get('Branche')]]})
                        all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Chat Branche Konfidenz"]["index"] + 1)}{row_num}', 'values': [[result.get('Konfidenz')]]})
                        all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"]["index"] + 1)}{row_num}', 'values': [[result.get('Begruendung')]]})
                    else:
                        all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"]["index"] + 1)}{row_num}', 'values': [['FEHLER (Batch-Antwort)']]} )
                    all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"]["index"] + 1)}{row_num}', 'values': [[now_timestamp]]})
            else:
                self.logger.error(f"Batch-Verarbeitung fehlgeschlagen. Setze Fehlerstatus.")
                for task in batch_tasks:
                    row_num = task['row_num']
                    all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"]["index"] + 1)}{row_num}', 'values': [['FEHLER (Batch-API)']]} )
                    all_sheet_updates.append({'range': f'{_get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"]["index"] + 1)}{row_num}', 'values': [[now_timestamp]]})

        if all_sheet_updates:
            self.logger.info(f"Sende finales Batch-Update für {len(tasks)} bewertete Branchen...")
            self.sheet_handler.batch_update_cells(all_sheet_updates)

        self.logger.info("Branchen-Neubewertung im Batch-Modus abgeschlossen.")


    # ==========================================================================
    # === Batch Processing Methods ===========================================
    # ==========================================================================


    @retry_on_failure
    def _process_verification_openai_batch(self, batch_data):
        """
        Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
        """
        if not batch_data:
            return {}
        self.logger.debug(
            f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen)...")

        aggregated_prompt = (
            "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. "
            "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel plausibel zum Firmennamen und zur Beschreibung passt. "
            "Gib das Ergebnis fuer jeden Eintrag ausschliesslich im folgenden Format auf einer neuen Zeile aus:\n"
            "Eintrag <Zeilennummer>: <Antwort>\n\n"
            "Moegliche Antworten:\n"
            "- 'OK' (wenn der Artikel gut passt)\n"
            "- 'X | Alternativer Artikel: <URL> | Begruendung: <Kurze Begruendung>'\n"
            "- 'X | Kein passender Artikel gefunden | Begruendung: <Kurze Begruendung>'\n\n"
            "Eintraege zur Pruefung:\n--------------------\n")
        max_desc_length = 200
        for item in batch_data:
            entry_text = (
                f"Eintrag {item['row_num']}:\n"
                f"  Firmenname: {str(item.get('company_name', 'k.A.'))}\n"
                f"  CRM-Beschreibung: {str(item.get('crm_desc', 'k.A.'))[:max_desc_length]}\n"
                f"  Wikipedia-URL: {str(item.get('wiki_url', 'k.A.'))}\n"
                f"  Wiki-Absatz: {str(item.get('wiki_paragraph', 'k.A.'))[:max_desc_length]}\n"
                f"  Wiki-Kategorien: {str(item.get('wiki_categories', 'k.A.'))[:max_desc_length]}\n----\n")
            aggregated_prompt += entry_text

        try:
            # KORREKTUR: Direkter Aufruf des robusten Helpers 'call_openai_chat' aus helpers.py
            from helpers import call_openai_chat
            chat_response = call_openai_chat(
                aggregated_prompt, temperature=0.0)
            if not chat_response:
                raise APIError(
                    "Keine Antwort von OpenAI erhalten.", request=None, body=None)
        except Exception as e:
            self.logger.error(
                f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung: {e}")
            return {
                item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data}

        answers = {}
        original_batch_row_nums = {item['row_num'] for item in batch_data}
        lines = chat_response.strip().split('\n')
        for line in lines:
            match = re.match(r"Eintrag (\d+): (.*)", line.strip())
            if match:
                row_num, answer_text = int(
                    match.group(1)), match.group(2).strip()
                if row_num in original_batch_row_nums:
                    answers[row_num] = answer_text
        for row_num in original_batch_row_nums:
            if row_num not in answers:
                answers[row_num] = "FEHLER: Antwort nicht geparst"
        return answers


    # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) ---
    # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI,
    # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet.
    # Basierend auf process_verification_only und _process_batch aus Teil 8.
    # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block).
    # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time.
    # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).

    def process_verification_batch(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert).
        Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.)
        bereits gesetzt ist, ob eine Wiki URL (M) vorhanden ist und ob Status S
        nicht bereits 'OK', 'X (URL Copied)' oder 'X (Invalid Suggestion)' ist.
        Setzt AX + AP fuer bearbeitete Zeilen und schreibt S-U in Batches.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AX).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge die Konfiguration des Batch-Laufs
        self.logger.info(
            f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT

        # --- Daten laden und Startzeile ermitteln ---
        # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
        if start_sheet_row is None:
            self.logger.info(
                "Automatische Ermittlung der Startzeile basierend auf leeren AX...")  # <<< GEÄNDERT
            # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map).
            # Standardmaessig ab Zeile 7
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key="Wiki Verif. Timestamp", min_sheet_row=7)

            # Wenn get_start_row_index -1 zurueckgibt (Fehler)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")  # <<< GEÄNDERT
                return  # Beende die Methode

            # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten
            # Daten-Index
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1  # Block 14 SheetHandler Attribut
            self.logger.info(
                f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}")  # <<< GEÄNDERT
        else:
            # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
            # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block
            # 2).
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "FEHLER beim Laden der Daten fuer process_verification_batch.")  # <<< GEÄNDERT
                return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den verarbeitungsbereich
        self.logger.info(
            f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht
        # ueber Gesamtzeilen)
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes und Buchstaben ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = [
            # Pruefkriterien / Timestamp (AX, M, S)
            "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung",
            # Daten fuer Prompt (B, F, N, R)
            "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien",
            # Ergebnisspalten (T, U)
            "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel",
            # Spalten V-Y zum Leeren
            "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche",
            "Wikipedia Timestamp", "Timestamp letzte Pruefung",  # Spalten AN, AO zum Leeren
            "Version", "SerpAPI Wiki Search Timestamp"  # Spalten AP, AY zum Leeren
        ]
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt
        # interne Helfer _get_col_letter Block 14)
        ts_ax_letter = _get_col_letter(
            col_indices["Wiki Verif. Timestamp"] + 1)  # Timestamp zu setzen (AX)
        s_letter = _get_col_letter(
            col_indices["Chat Wiki Konsistenzpruefung"] + 1)  # Status S
        t_letter = _get_col_letter(
            col_indices["Chat Begründung Wiki Inkonsistenz"] + 1)  # Begruendung T
        u_letter = _get_col_letter(
            col_indices["Chat Vorschlag Wiki Artikel"] + 1)  # Vorschlag U

        # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt).
        # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki).
        # Y ist Begruendung Abweichung Branche (von Chat).
        v_idx = col_indices["Begruendung bei Abweichung"]
        # Block 1 Column Map
        y_idx = col_indices["Chat Begruendung Abweichung Branche"]
        # Erstellen Sie den Bereichsnamen (z.B. "V:Y")
        v_letter = _get_col_letter(v_idx + 1)
        y_letter = _get_col_letter(y_idx + 1)
        v_y_range_letter = f'{v_letter}:{y_letter}'  # z.B. V:Y
        # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
        # Anzahl der Spalten = Y_Index - V_Index + 1
        empty_vy_values = [''] * (y_idx - v_idx + 1)

        # Timestamps AN, AO, AP, AY leeren.
        # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden,
        # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen
        # Schritten erneut bearbeitet wird.
        an_letter = _get_col_letter(
            col_indices["Wikipedia Timestamp"] + 1)  # AN (Wiki Extraction TS)
        ao_letter = _get_col_letter(
            col_indices["Timestamp letzte Pruefung"] +
            1)  # AO (Chat Evaluation TS)
        ap_letter = _get_col_letter(
            col_indices["Version"] + 1)  # AP (Version)
        ay_letter = _get_col_letter(
            col_indices["SerpAPI Wiki Search Timestamp"] +
            1)  # AY (SerpAPI Wiki TS)

        # --- Verarbeitung ---
        # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1)
        # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization
        openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20)
        # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1)
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts)
        current_openai_batch_data = []
        # 1-basierte Zeilennummern im aktuellen OpenAI Batch
        rows_in_current_openai_batch = []
        # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
        all_sheet_updates = []

        # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den
        # Batch aufgenommen werden (im Rahmen des Limits).
        processed_count = 0
        # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender
        # Daten etc.).
        skipped_count = 0
        # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen
        # wurden.
        skipped_no_wiki_url = 0

        # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden)
            # Nutzt interne Helfer _get_cell_value_safe
            company_name = self._get_cell_value_safe(
                row, "CRM Name").strip()  # Block 1 Column Map
            if not company_name:
                self.logger.debug(
                    f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).")  # <<< GEÄNDERT
                skipped_count += 1  # Zaehlen als uebersprungen
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: Wiki Verif. Timestamp (AX) ist leer
            # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.)
            # UND Status S ist NICHT bereits in einem Endzustand (OK, X
            # (UPDATED/COPIED/INVALID)).

            # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne
            # Helfer)
            ax_value = self._get_cell_value_safe(
                row, "Wiki Verif. Timestamp").strip()  # Block 1 Column Map
            m_value = self._get_cell_value_safe(
                row, "Wiki URL").strip()  # Block 1 Column Map
            s_value_upper = self._get_cell_value_safe(
                row, "Chat Wiki Konsistenzpruefung").strip().upper()  # Block 1 Column Map

            # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht
            is_wiki_url_valid_looking = m_value and isinstance(
                m_value,
                str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in [
                "k.a.",
                "kein artikel gefunden",
                "fehler bei suche",
                "http:"]  # Fuege "http:" hinzu basierend auf Log

            # Definieren Sie die Endzustaende von Status S (Grossbuchstaben)
            s_end_states = [
                "OK",
                "X (UPDATED)",
                "X (URL COPIED)",
                "X (INVALID SUGGESTION)"]
            # Pruefen Sie, ob Status S in einem Endzustand ist
            is_s_in_endstate = s_value_upper in s_end_states  # Bugfix: Korrekte Zuweisung

            # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig
            # aussieht UND S NICHT im Endzustand ist.
            processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                self.logger.debug(
                    f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender
                # M-URL uebersprungen wurde
                if not is_wiki_url_valid_looking:
                    skipped_no_wiki_url += 1
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu ---
            # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im
            # Rahmen des Limits zaehlen)
            processed_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            # Sammle die benoetigten Daten fuer den OpenAI Prompt (Block 26 - _process_verification_openai_batch).
            # Diese Daten werden in einem Dictionary fuer den Batch gesammelt.
            crm_desc = self._get_cell_value_safe(
                row, "CRM Beschreibung")  # Block 1 Column Map
            wiki_paragraph = self._get_cell_value_safe(
                row, "Wiki Absatz")  # Block 1 Column Map
            wiki_categories = self._get_cell_value_safe(
                row, "Wiki Kategorien")  # Block 1 Column Map

            # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer
            # OpenAI hinzu
            current_openai_batch_data.append({
                'row_num': i,  # Die 1-basierte Sheet-Zeilennummer
                'company_name': company_name,  # Nutzt den initial geladenen Namen
                'crm_desc': crm_desc,
                'wiki_url': m_value,  # Nutzt die M-URL aus dem Sheet
                'wiki_paragraph': wiki_paragraph,
                'wiki_categories': wiki_categories
            })
            # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu
            rows_in_current_openai_batch.append(i)

            # --- Verarbeite den Batch, wenn voll ---
            # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat.
            # openai_batch_size wird aus Config geholt (Block 1).
            if len(current_openai_batch_data) >= openai_batch_size:
                # Logge den Start der Batch-Verarbeitung
                batch_start_row = current_openai_batch_data[0]['row_num']
                batch_end_row = current_openai_batch_data[-1]['row_num']
                self.logger.debug(
                    f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")  # <<< GEÄNDERT

                # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht.
                # _process_verification_openai_batch (Block 26) ist mit retry_on_failure dekoriert.
                # Wenn _process_verification_openai_batch eine Exception wirft
                # (nach Retries), wird diese hier gefangen.
                batch_results = self._process_verification_openai_batch(
                    current_openai_batch_data)
                # Ergebnisse sollten ein Dictionary {row_num:
                # raw_chatgpt_answer} sein, auch bei Fehlern.

                # Sammle Sheet Updates basierend auf den Batch-Ergebnissen.
                # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y.
                # Der aktuelle Zeitstempel fuer den Batch
                current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                batch_sheet_updates = []  # Updates fuer DIESEN spezifischen Batch von Zeilen

                # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch
                # waren
                for row_num in rows_in_current_openai_batch:
                    # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary.
                    # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt
                    # (sollte nicht passieren, wenn
                    # _process_verification_openai_batch korrekt ist).
                    answer = batch_results.get(
                        row_num, "FEHLER: Batch-Ergebnis fehlt")
                    # self.logger.debug(f"Zeile {row_num}
                    # Verifizierungsantwort: '{answer[:100]}...'") # Zu viel
                    # Laerm (gekuerzt)

                    # Logik zur Bestimmung der Werte fuer S, T, U basierend auf
                    # 'answer' (aehnlich wie in altem _process_batch)
                    # Initialisiere mit leeren Strings
                    wiki_confirm, alt_article, wiki_explanation = "", "", ""

                    # Pruefe auf Standard-Antworten und Fehler-Antworten
                    if isinstance(answer, str) and answer.upper() == "OK":
                        wiki_confirm = "OK"
                        wiki_explanation = "Passt laut KI zur Firma."  # Standard Begruendung bei OK
                    elif isinstance(answer, str) and answer.startswith("X |"):
                        # Parse die Antwort im Format "X | <Detail> |
                        # <Begruendung>"
                        # Teile maximal in 3 Teile
                        parts = answer.split("|", 2)
                        wiki_confirm = "X"  # Status ist X
                        if len(parts) > 1:
                            # Zweiter Teil ist Detail (Alternative URL oder
                            # "Kein passender Artikel gefunden")
                            detail = parts[1].strip()
                            if detail.lower().startswith("alternativer artikel:"):
                                alt_article = detail.split(
                                    ":", 1)[1].strip()  # Extrahiere URL
                            elif detail.lower() == "kein passender artikel gefunden":
                                alt_article = detail  # Text "Kein passender Artikel gefunden"
                            else:
                                alt_article = detail  # Unbekanntes Detail

                        if len(parts) > 2:
                            # Dritter Teil ist Begruendung
                            reason_part = parts[2].strip()
                            if reason_part.lower().startswith("begruendung:"):
                                wiki_explanation = reason_part.split(
                                    ":", 1)[1].strip()  # Extrahiere Begruendungstext
                            else:
                                wiki_explanation = reason_part  # Unbekannte Begruendung

                        # Fuege ggf. den rohen Antworttext zur Begruendung
                        # hinzu, wenn Parsing unvollstaendig war
                        if not alt_article or not wiki_explanation:
                            wiki_explanation += f" (Rohantwort: {answer[:100]}...)"

                    elif isinstance(answer, str) and answer.startswith("FEHLER"):
                        # Wenn die Batch-Verarbeitung einen Fehler
                        # zurueckgegeben hat
                        wiki_confirm = "FEHLER"
                        wiki_explanation = answer  # Fehlermeldung in Begruendung schreiben
                        alt_article = "Siehe Begruendung"  # Verweis auf Begruendung

                    # Unerwartetes Format der Antwort (weder OK noch X | noch
                    # FEHLER)
                    else:
                        wiki_confirm = "?"  # Setze Status auf unbekannt
                        # Speichere Anfang der Antwort in Begruendung
                        # (gekuerzt)
                        wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."
                        alt_article = "Siehe Begruendung"  # Verweis auf Begruendung

                    # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert
                    # Fuer jede Zeile im Batch fuegen wir das Update hinzu.
                    # empty_vy_values wurde oben vorbereitet.
                    v_y_values = empty_vy_values  # Liste von leeren Strings
                    # Fuege Update zum Leeren von V-Y hinzu, falls Index
                    # gefunden wurde
                    if v_y_range_letter:  # Pruefe, ob der Bereichsname ermittelt werden konnte
                        batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [
                                                   v_y_values]})  # Block 1 Column Map, interne Helfer

                    # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne
                    # Helfer)
                    batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [
                                               [wiki_confirm]]})  # Block 1 Column Map
                    batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [
                                               [alt_article]]})  # Block 1 Column Map
                    batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [
                                               [wiki_explanation]]})  # Block 1 Column Map
                    # Setze AX Timestamp fuer diese Zeile
                    batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [
                                               [current_batch_timestamp]]})  # Block 1 Column Map

                # --- Sende gesammelte Updates fuer diesen Batch ---
                # Sammle die Updates fuer diesen Batch in der globalen Liste.
                # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier
                # sammeln, sondern direkt senden

                # Sende die gesammelten Updates fuer DIESEN Batch sofort.
                if batch_sheet_updates:
                    self.logger.debug(
                        f"  Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...")  # <<< GEÄNDERT
                    # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
                    # Wenn es fehlschlaegt, wird es intern geloggt.
                    success = self.sheet_handler.batch_update_cells(
                        batch_sheet_updates)
                    if success:
                        self.logger.info(
                            f"  Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.")  # <<< GEÄNDERT
                    # Der Fehlerfall wird von batch_update_cells geloggt

                # Setze Batch-Listen zurueck fuer die naechste Iteration
                current_openai_batch_data = []
                rows_in_current_openai_batch = []

                # Pause nach jedem OpenAI Batch (nutzt Config Block 1).
                # Dies ist wichtig, um Rate Limits zu vermeiden.
                # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war
                pause_duration = getattr(
                    Config, 'RETRY_DELAY', 5) * 0.5  # 50% der Retry-Wartezeit
                self.logger.debug(
                    f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---")  # <<< GEÄNDERT
                time.sleep(pause_duration)

        # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife ---
        # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind
        if current_openai_batch_data:
            # Logge den Start des finalen Batches
            batch_start_row = current_openai_batch_data[0]['row_num']
            batch_end_row = current_openai_batch_data[-1]['row_num']
            self.logger.debug(
                f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")  # <<< GEÄNDERT

            # Rufe die interne Methode auf, die den OpenAI Call macht
            batch_results = self._process_verification_openai_batch(
                current_openai_batch_data)
            # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer}
            # sein, auch bei Fehlern.

            # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch
            current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            batch_sheet_updates = []  # Updates fuer DIESEN spezifischen Batch von Zeilen

            # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI
            # Batch waren
            for row_num in rows_in_current_openai_batch:
                # Hole das Ergebnis fuer diese Zeile aus dem
                # Ergebnis-Dictionary.
                answer = batch_results.get(
                    row_num, "FEHLER: Batch-Ergebnis fehlt")  # Fallback

                # Logik zur Bestimmung der Werte fuer S, T, U basierend auf
                # 'answer'
                wiki_confirm, alt_article, wiki_explanation = "", "", ""
                # Leere V-Y Spalten
                v_y_values = empty_vy_values  # Liste von leeren Strings
                if isinstance(answer, str) and answer.upper() == "OK":
                    wiki_confirm = "OK"
                    wiki_explanation = "Passt laut KI zur Firma."
                elif isinstance(answer, str) and answer.startswith("X |"):
                    parts = answer.split("|", 2)
                    wiki_confirm = "X"
                    if len(parts) > 1:
                        detail = parts[1].strip()
                        alt_article = detail.split(":", 1)[1].strip() if detail.lower(
                        ).startswith("alternativer artikel:") else detail
                    if len(parts) > 2:
                        reason_part = parts[2].strip()
                        wiki_explanation = reason_part.split(":", 1)[1].strip(
                        ) if reason_part.lower().startswith("begruendung:") else reason_part
                    if not alt_article or not wiki_explanation:
                        wiki_explanation += f" (Rohantwort: {answer[:100]}...)"
                elif isinstance(answer, str) and answer.startswith("FEHLER"):
                    wiki_confirm = "FEHLER"
                    wiki_explanation = answer
                    alt_article = "Siehe Begruendung"
                else:
                    wiki_confirm = "?"
                    wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."
                    alt_article = "Siehe Begruendung"

                # Fuege Updates fuer S, T, U und AX hinzu
                batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [
                                           [wiki_confirm]]})  # Block 1 Column Map
                batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [
                                           [alt_article]]})  # Block 1 Column Map
                batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [
                                           [wiki_explanation]]})  # Block 1 Column Map
                # Setze AX Timestamp
                batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [
                                           [current_batch_timestamp]]})  # Block 1 Column Map

                # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden
                # wurde
                if v_y_range_letter:
                    batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [
                                               v_y_values]})  # Block 1 Column Map, interne Helfer

            # Sende die gesammelten Updates fuer DIESEN finalen Batch.
            if batch_sheet_updates:
                self.logger.debug(
                    f"  Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
                # Nutzt die batch_update_cells Methode des Sheet Handlers
                # (Block 14) mit Retry.
                success = self.sheet_handler.batch_update_cells(
                    batch_sheet_updates)
                if success:
                    self.logger.info(
                        f"  FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.")  # <<< GEÄNDERT
                # Der Fehlerfall wird von batch_update_cells geloggt

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).")  # <<< GEÄNDERT


    def process_summarize_website(self, start_sheet_row=None, end_sheet_row=None, limit=None):
        """
        Batch-Prozess NUR für Website-Zusammenfassung.
        Nutzt einen ThreadPoolExecutor für echte parallele Verarbeitung und verbesserte Stabilität.
        """
        self.logger.info(f"Starte Website-Zusammenfassung (Parallel Batch, v2.0.3). Bereich: {start_sheet_row or 'Start'}-{end_sheet_row or 'Ende'}, Limit: {limit or 'Unbegrenzt'}")

        # --- 1. Daten laden und Startzeile ermitteln ---
        if start_sheet_row is None:
            self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren 'Website Zusammenfassung'...")
            start_data_idx = self.sheet_handler.get_start_row_index(check_column_key="Website Zusammenfassung")
            if start_data_idx == -1:
                self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
                return
            start_sheet_row = start_data_idx + self.sheet_handler._header_rows + 1
            self.logger.info(f"Automatisch ermittelte Startzeile: {start_sheet_row}")
        
        if not self.sheet_handler.load_data():
            self.logger.error("FEHLER beim Laden der Daten für Summarization-Batch.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)
        effective_end_row = end_sheet_row if end_sheet_row is not None else total_sheet_rows
        
        self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {effective_end_row}.")
        if start_sheet_row > effective_end_row:
             self.logger.info("Start liegt nach dem Ende. Keine Zeilen zu verarbeiten.")
             return

        # --- 2. Spalten-Indizes und Buchstaben vorbereiten ---
        summary_col_letter = _get_col_letter(get_col_idx("Website Zusammenfassung") + 1)
        version_col_letter = _get_col_letter(get_col_idx("Version") + 1)
        # Wir benötigen auch einen Timestamp für die Zusammenfassung. Da keiner existiert, nutzen wir den Scrape-Timestamp neu.
        timestamp_col_letter = _get_col_letter(get_col_idx("Website Scrape Timestamp") + 1)


        # --- 3. Tasks sammeln ---
        openai_batch_size = getattr(Config, 'OPENAI_BATCH_SIZE_LIMIT', 10) # Kann höher sein, da parallel
        max_openai_workers = getattr(Config, 'MAX_SCRAPING_WORKERS', 10) # Gleiche Worker-Anzahl wie beim Scraping
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        tasks_for_processing_batch = []
        all_sheet_updates = []
        processed_count = 0
        skipped_count = 0

        for i in range(start_sheet_row, effective_end_row + 1):
            row_index_in_list = i - 1
            if row_index_in_list >= total_sheet_rows: break
            
            row = all_data[row_index_in_list]
            if not any(cell and str(cell).strip() for cell in row):
                 skipped_count += 1
                 continue

            # Kriterium: Zusammenfassung ist leer/default UND Rohtext ist valide
            summary_value = self._get_cell_value_safe(row, "Website Zusammenfassung")
            summary_is_empty_or_default = not summary_value or str(summary_value).strip().lower() in ["k.a.", "k.a. (keine zusammenfassung erhalten)"]
            
            raw_text = self._get_cell_value_safe(row, "Website Rohtext")
            raw_text_is_valid = raw_text and isinstance(raw_text, str) and len(raw_text) > 100 and not str(raw_text).strip().lower().startswith('k.a.')

            if summary_is_empty_or_default and raw_text_is_valid:
                if limit is not None and processed_count >= limit:
                    self.logger.info(f"Verarbeitungslimit ({limit}) erreicht.")
                    break
                
                company_name = self._get_cell_value_safe(row, "CRM Name")
                tasks_for_processing_batch.append({"row_num": i, "raw_text": raw_text, "company_name": company_name})
                processed_count += 1
            else:
                skipped_count += 1

            # --- 4. Batch-Verarbeitung auslösen ---
            if len(tasks_for_processing_batch) >= openai_batch_size or (i == effective_end_row and tasks_for_processing_batch):
                self.logger.info(f"--- Starte Website-Summarization Batch für {len(tasks_for_processing_batch)} Tasks (max. {max_openai_workers} Worker) ---")
                
                summarization_results = {}
                batch_error_count = 0
                
                with ThreadPoolExecutor(max_workers=max_openai_workers) as executor:
                    future_to_task = {executor.submit(self._summarize_task_batch, task): task for task in tasks_for_processing_batch}
                    
                    for future in as_completed(future_to_task):
                        task = future_to_task[future]
                        try:
                            result_dict = future.result()
                            if isinstance(result_dict, dict) and 'row_num' in result_dict:
                                summarization_results[result_dict['row_num']] = result_dict
                                if result_dict.get('error'):
                                    batch_error_count += 1
                                    self.logger.warning(f"Worker meldete Fehler bei Zusammenfassung für Zeile {result_dict['row_num']}: {result_dict.get('status_message')}")
                            else:
                                self.logger.error(f"Inkonsistentes Ergebnis für Zeile {task['row_num']}: Erwartete dict mit 'row_num', bekam {type(result_dict)}. Überspringe.")
                                summarization_results[task['row_num']] = {'summary': "FEHLER (Inkonsistenter Rückgabetyp)", 'error': True}
                                batch_error_count += 1
                        except Exception as exc:
                            self.logger.error(f"Unerwarteter Fehler bei Ergebnisabfrage für Zeile {task['row_num']}: {exc}", exc_info=True)
                            summarization_results[task['row_num']] = {'summary': "FEHLER (Task Exception)", 'error': True}
                            batch_error_count += 1
                
                self.logger.info(f"  -> Zusammenfassung für Batch beendet. {len(summarization_results)} Ergebnisse erhalten ({batch_error_count} davon mit Fehlern).")

                # --- 5. Updates für das Google Sheet vorbereiten ---
                if summarization_results:
                    current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    current_version = getattr(Config, 'VERSION', 'unknown')
                    
                    for row_num, res_dict in summarization_results.items():
                        # Zusammenfassung, Timestamp (wir überschreiben den alten Scrape-TS) und Version werden zum Update hinzugefügt
                        all_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [[res_dict.get('summary', 'k.A.')]]})
                        all_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [[current_timestamp]]})
                        all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]})

                tasks_for_processing_batch = [] # Batch leeren

                # --- 6. Sheet-Update auslösen, wenn Update-Batch voll ist ---
                # Pro Zeile gibt es 3 Updates
                if len(all_sheet_updates) >= (update_batch_row_limit * 3):
                    self.logger.info(f"Sende gesammelte Sheet-Updates für Zusammenfassungen ({len(all_sheet_updates) // 3} Zeilen)...")
                    self.sheet_handler.batch_update_cells(all_sheet_updates)
                    all_sheet_updates = []
                    time.sleep(1)

        # --- 7. Finale Updates senden ---
        if all_sheet_updates:
            self.logger.info(f"Sende finale gesammelte Sheet-Updates für Zusammenfassungen ({len(all_sheet_updates) // 3} Zeilen)...")
            self.sheet_handler.batch_update_cells(all_sheet_updates)

        self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen zur Verarbeitung ausgewählt, {skipped_count} Zeilen übersprungen.")



    def evaluate_branch_task(self, task_data, openai_semaphore):
        """
        Führt die Branchenevaluation fuer eine einzelne Zeile aus.
        """
        logger = logging.getLogger(__name__ + ".evaluate_branch_task")
        row_num = task_data['row_num']
        result = {
            "branch": "k.A. (Fehler Task)",
            "consistency": "error",
            "justification": "Fehler in Worker-Task"}
        error = None
        try:
            with openai_semaphore:
                result = evaluate_branche_chatgpt(
                    task_data['crm_branche'], task_data['beschreibung'],
                    task_data['wiki_branche'], task_data['wiki_kategorien'],
                    task_data['website_summary']
                )
        except Exception as e:
            error = f"Fehler bei Branchenevaluation Zeile {row_num}: {e}"
            logger.error(error)
            logger.debug(traceback.format_exc())
            result = {"branch": "FEHLER",
                      "consistency": "error_task",
                      "justification": error[:500]}
        return {"row_num": row_num, "result": result, "error": error}

    def process_branch_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None):
        """
        Batch-Prozess NUR fuer Brancheneinschaetzung.
        """
        global ALLOWED_TARGET_BRANCHES
        self.logger.info(
            f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        if not ALLOWED_TARGET_BRANCHES:
            load_target_schema()
        if not ALLOWED_TARGET_BRANCHES:
            self.logger.critical(
                "FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.")
            return
        self.logger.info(
            f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")

        # --- Daten laden und Startzeile ermitteln ---
        if start_sheet_row is None:
            self.logger.info(
                "Automatische Ermittlung der Startzeile basierend auf leeren Timestamp letzte Pruefung (BC)...")
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key="Timestamp letzte Pruefung", min_sheet_row=7)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
                return
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1
            self.logger.info(
                f"Automatisch ermittelte Startzeile (erste leere BC Zelle): {start_sheet_row}")
        else:
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "FEHLER beim Laden der Daten fuer process_branch_batch.")
                return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)

        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows
        self.logger.info(
            f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
            return

        # --- Indizes und Buchstaben ---
        required_keys = [
            "Timestamp letzte Pruefung",
            "CRM Branche",
            "CRM Beschreibung",
            "Wiki Branche",
            "Wiki Kategorien",
            "Website Zusammenfassung",
            "Version",
            "Chat Vorschlag Branche",
            "Chat Branche Konfidenz",
            "Chat Konsistenz Branche",
            "Chat Begruendung Abweichung Branche",
            "CRM Name"]
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.")
            return

        MAX_BRANCH_WORKERS = getattr(Config, 'MAX_BRANCH_WORKERS', 10)
        OPENAI_CONCURRENCY_LIMIT = getattr(
            Config, 'OPENAI_CONCURRENCY_LIMIT', 3)
        processing_batch_size = getattr(
            Config, 'PROCESSING_BRANCH_BATCH_SIZE', 20)

        tasks_for_current_batch = []
        processed_tasks_count = 0  # Zählt Tasks, die tatsächlich verarbeitet wurden
        skipped_count = 0

        if not ALLOWED_TARGET_BRANCHES:
            load_target_schema()
            if not ALLOWED_TARGET_BRANCHES:
                self.logger.critical(
                    "FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.")
                return

        # Funktion zum Verarbeiten eines einzelnen Batches (um Code-Duplikation
        # zu reduzieren)
        def _execute_and_write_batch(batch_tasks_to_run):
            nonlocal processed_tasks_count  # Zugriff auf die äußere Variable
            if not batch_tasks_to_run:
                return

            batch_start_log = batch_tasks_to_run[0]['row_num']
            batch_end_log = batch_tasks_to_run[-1]['row_num']
            self.logger.debug(
                f"\n--- Verarbeite Branch-Evaluation Batch ({len(batch_tasks_to_run)} Tasks, Zeilen {batch_start_log}-{batch_end_log}) ---")

            current_batch_results = []
            current_batch_errors = 0
            openai_sem = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT)

            with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_BRANCH_WORKERS) as executor:
                future_map = {
                    executor.submit(
                        self.evaluate_branch_task,
                        task,
                        openai_sem): task for task in batch_tasks_to_run}
                for future in concurrent.futures.as_completed(future_to_task):
                        task = future_to_task[future]
                        try:
                            # HIER KOMMT JETZT GARANTIERT EIN DICTIONARY AN
                            result_dict = future.result()
                            
                            # Prüfe explizit, ob es wirklich ein Dictionary ist (doppelte Sicherheit)
                            if not isinstance(result_dict, dict):
                                self.logger.error(f"Fehlerhaftes Ergebnis für Zeile {task['row_num']}: Worker gab keinen Dictionary zurück. Bekam {type(result_dict)}. Überspringe.")
                                batch_error_count += 1
                                continue

                            # Lese die Werte aus dem Dictionary, anstatt das ganze Objekt zu speichern
                            row_num_from_result = result_dict['row_num']
                            raw_text_res = result_dict['raw_text']
                            
                            scraping_results[row_num_from_result] = raw_text_res
                            
                            if result_dict.get('error'):
                                batch_error_count += 1

                        except Exception as exc:
                            row_num = task['row_num']
                            err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage für Zeile {row_num} ({task['url'][:100]}): {exc}"
                            self.logger.error(err_msg)
                            scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)"
                            batch_error_count += 1
                        self.logger.error(
                            f"Exception im Future für Zeile {task_info['row_num']}: {exc_future}")
                        current_batch_results.append(
                            {
                                "row_num": task_info['row_num'],
                                "result": {
                                    "branch": "FEHLER FUTURE",
                                    "consistency": "error_task",
                                    "justification": str(exc_future)[
                                        :100]},
                                "error": str(exc_future)})
                        current_batch_errors += 1

            self.logger.debug(
                f"  Batch ({batch_start_log}-{batch_end_log}) beendet. {len(current_batch_results)} Ergebnisse, {current_batch_errors} Fehler.")

            if current_batch_results:
                ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                ver = getattr(Config, 'VERSION', 'unknown')
                updates_this_batch = []
                current_batch_results.sort(key=lambda x: x['row_num'])
                for item in current_batch_results:
                    rn, res = item['row_num'], item['result']
                    self.logger.debug(f"  Zeile {rn} (Ergebnis): {res}")
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Chat Vorschlag Branche") + 1)}{rn}',
                            'values': [
                                [
                                    res.get(
                                        "branch",
                                        "ERR BR")]]})
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Chat Branche Konfidenz") + 1)}{rn}',
                            'values': [
                                [
                                    res.get(
                                        "confidence",
                                        "N/A CO")]]})
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Chat Konsistenz Branche") + 1)}{rn}',
                            'values': [
                                [
                                    res.get(
                                        "consistency",
                                        "err CO")]]})
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Chat Begruendung Abweichung Branche") + 1)}{rn}',
                            'values': [
                                [
                                    res.get(
                                        "justification",
                                        "No JU")]]})
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Timestamp letzte Pruefung") + 1)}{rn}',
                            'values': [
                                [ts]]})
                    updates_this_batch.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Version") + 1)}{rn}',
                            'values': [
                                [ver]]})

                if updates_this_batch:
                    self.logger.debug(
                        f"  Sende Sheet-Update für {len(current_batch_results)} Zeilen dieses Batches...")
                    s_upd = self.sheet_handler.batch_update_cells(
                        updates_this_batch)
                    if s_upd:
                        self.logger.info(
                            f"  Sheet-Update für Batch Zeilen {batch_start_log}-{batch_end_log} erfolgreich.")

            # Zähle verarbeitete Tasks
            processed_tasks_count += len(batch_tasks_to_run)

            pause_dur = getattr(Config, 'RETRY_DELAY', 5) * 0.8
            self.logger.debug(
                f"--- Batch ({batch_start_log}-{batch_end_log}) abgeschlossen. Warte {pause_dur:.2f}s ---")
            time.sleep(pause_dur)
        # Ende der Hilfsfunktion _execute_and_write_batch

        # Hauptschleife über die Zeilen
        for i in range(start_sheet_row, end_sheet_row + 1):
            # Prüfe Limit für *tatsächlich verarbeitete* Tasks
            if limit is not None and processed_tasks_count >= limit:
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) erreicht. Stoppe weitere Zeilenprüfung.")
                break

            row_index_in_list = i - 1
            if row_index_in_list >= total_sheet_rows:
                break

            row = all_data[row_index_in_list]
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                skipped_count += 1
                continue

            company_name_log = self._get_cell_value_safe(
                row, "CRM Name").strip()
            ao_value = self._get_cell_value_safe(
                row, "Timestamp letzte Pruefung").strip()
            if ao_value:  # Wenn Timestamp gesetzt ist, überspringen
                skipped_count += 1
                continue

            # --- DEBUG BLOCK für info_sources_count (wie gehabt) ---
            crm_branche_val = self._get_cell_value_safe(
                row, "CRM Branche").strip()
            crm_beschreibung_val = self._get_cell_value_safe(
                row, "CRM Beschreibung").strip()
            wiki_branche_val = self._get_cell_value_safe(
                row, "Wiki Branche").strip()
            wiki_kategorien_val = self._get_cell_value_safe(
                row, "Wiki Kategorien").strip()
            website_summary_val = self._get_cell_value_safe(
                row, "Website Zusammenfassung").strip()
            # ... (kompletter detaillierter Debug-Block für info_sources_count hier einfügen) ...
            self.logger.debug(
                f"Zeile {i} ({company_name_log[:30]}...) - Rohwerte für Info-Quellen:")
            sources_to_check = {
                "CRM Branche": crm_branche_val,
                "CRM Beschreibung": crm_beschreibung_val,
                "Wiki Branche": wiki_branche_val,
                "Wiki Kategorien": wiki_kategorien_val,
                "Website Zusammenfassung": website_summary_val}
            info_sources_count = 0
            counted_sources = []
            for source_name, val in sources_to_check.items():
                cond1 = bool(val)
                cond2 = isinstance(val, str)
                cond3 = False
                cond4 = False
                cond5 = False
                if cond1 and cond2:
                    stripped_val = val.strip()
                    cond3 = bool(stripped_val)
                    if cond3:
                        lower_stripped_val = stripped_val.lower()
                        cond4 = lower_stripped_val != "k.a."
                        cond5 = not stripped_val.upper().startswith("FEHLER")
                is_valid_source = cond1 and cond2 and cond3 and cond4 and cond5
                if is_valid_source:
                    info_sources_count += 1
                    counted_sources.append(source_name)
                self.logger.debug(
                    f"  Prüfe Quelle '{source_name}': Wert='{str(val)[:30]}...', c1?{cond1}, c2?{cond2}, c3?{cond3}, c4?{cond4}, c5?{cond5} -> Gültig? {is_valid_source}")
            self.logger.debug(
                f"Zeile {i} ({company_name_log[:30]}...) - Gezählte valide Quellen: {info_sources_count} - {counted_sources}")

            if info_sources_count < 2:
                self.logger.info(
                    f"Zeile {i} ({company_name_log[:30]}...) (Branch Check): Uebersprungen (Timestamp BC leer, aber nur {info_sources_count} Informationsquellen verfuegbar: {counted_sources}). Mindestens 2 benoetigt.")
                skipped_count += 1
                continue

            # Task zur Liste hinzufügen, wenn alle Kriterien erfüllt sind UND
            # Limit noch nicht erreicht
            if limit is None or (
                    processed_tasks_count +
                    len(tasks_for_current_batch)) < limit:
                tasks_for_current_batch.append({
                    "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val,
                    "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val,
                    "website_summary": website_summary_val
                })
            elif limit is not None and (processed_tasks_count + len(tasks_for_current_batch)) >= limit:
                # Wenn das Hinzufügen dieses Tasks das Limit erreichen oder überschreiten würde,
                # füge ihn noch hinzu (wird im nächsten Batch-Check gekürzt)
                # und beende dann die Schleife
                tasks_for_current_batch.append({
                    "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val,
                    "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val,
                    "website_summary": website_summary_val
                })
                self.logger.info(
                    f"Zeile {i} wurde als letzter Task vor Erreichen des Limits ({limit}) gesammelt.")
                # Die execute_and_write_batch Logik wird getriggert, wenn die
                # Schleife endet oder der Batch voll ist.

            # Batch ausführen, wenn voll ODER es die letzte Zeile ist
            if len(
                    tasks_for_current_batch) >= processing_batch_size or i == end_sheet_row:
                _execute_and_write_batch(tasks_for_current_batch)
                tasks_for_current_batch = []  # Batch-Liste für den nächsten Durchlauf leeren

        # Sicherstellen, dass ein eventuell nicht voller letzter Batch auch noch verarbeitet wird,
        # falls die Schleife durch das Limit beendet wurde und noch Tasks übrig
        # sind.
        if tasks_for_current_batch:
            self.logger.debug(
                f"Verarbeite verbleibenden Rest-Batch von {len(tasks_for_current_batch)} Tasks...")
            _execute_and_write_batch(tasks_for_current_batch)

        self.logger.info(
            f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_tasks_count} Zeilen verarbeitet, {skipped_count} Zeilen uebersprungen.")

    def process_find_wiki_serp(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None,
            min_employees=500,
            min_umsatz=200):
        """
        Sucht fehlende Wikipedia-URLs ueber SerpAPI.
        """
        self.logger.info(
            f"Starte Modus 'find_wiki_serp'. Filter: (Umsatz > {min_umsatz} MIO € ODER MA > {min_employees}). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        """
        Sucht fehlende Wikipedia-URLs (Spalte M = k.A.) ueber SerpAPI fuer Unternehmen mit
        (Umsatz CRM > min_umsatz MIO € ODER Mitarbeiter CRM > min_employees)
        UND wenn der SerpAPI Wiki Search Timestamp (AY) leer ist.
        Traegt gefundene URLs in Spalte M ein. Setzt ReEval-Flag (A)
        und loescht abhaengige Wiki-Spalten (N-V, AN, AO, AP, AX).
        Setzt Timestamp in Spalte AY, wann die Suche durchgefuehrt wurde (unabhaengig vom Ergebnis).

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AY).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
            min_employees (int, optional): Mindestanzahl Mitarbeiter (Spalte K) als Teilfilter. Defaults to 500.
            min_umsatz (int, optional): Mindestumsatz in MIO € (Spalte J) als Teilfilter. Defaults to 200.
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge die Konfiguration des Batch-Laufs
        self.logger.info(
            f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT

        # --- Daten laden und Startzeile ermitteln ---
        # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
        if start_sheet_row is None:
            self.logger.info(
                "Automatische Ermittlung der Startzeile basierend auf leeren AY...")  # <<< GEÄNDERT
            # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AY (Block 1 Column Map).
            # Standardmaessig ab Zeile 7
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key="SerpAPI Wiki Search Timestamp", min_sheet_row=7)

            # Wenn get_start_row_index -1 zurueckgibt (Fehler)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")  # <<< GEÄNDERT
                return  # Beende die Methode

            # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten
            # Daten-Index
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1  # Block 14 SheetHandler Attribut
            self.logger.info(
                f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}")  # <<< GEÄNDERT
        else:
            # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
            # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block
            # 2).
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "FEHLER beim Laden der Daten fuer process_find_wiki_serp.")  # <<< GEÄNDERT
                return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den verarbeitungsbereich
        self.logger.info(
            f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht
        # ueber Gesamtzeilen)
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes und Buchstaben ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = [
            # AY, M, J, K (Pruefkriterien / Timestamp)
            "SerpAPI Wiki Search Timestamp", "Wiki URL", "CRM Umsatz", "CRM Anzahl Mitarbeiter",
            # A, B, D (Daten fuer Suche / Updates)
            "ReEval Flag", "CRM Name", "CRM Website",
            # N-R (Spalten zum Leeren)
            "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien",
            # S-U (Spalten zum Leeren)
            "Chat Wiki Konsistenzpruefung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel",
            # V, AN, AO (Spalten zum Leeren)
            "Begruendung bei Abweichung", "Wikipedia Timestamp", "Timestamp letzte Pruefung",
            "Version", "Wiki Verif. Timestamp"  # AP, AX (Spalten zum Leeren)
        ]
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt
        # interne Helfer _get_col_letter Block 14)
        ts_ay_letter = _get_col_letter(
            col_indices["SerpAPI Wiki Search Timestamp"] +
            1)  # Timestamp zu setzen (AY)
        m_letter = _get_col_letter(
            col_indices["Wiki URL"] + 1)  # Wiki URL Spalte (M)
        a_letter = _get_col_letter(
            col_indices["ReEval Flag"] + 1)  # ReEval Flag (A)

        # Spalten N-V leeren.
        # N ist Wiki Absatz, V ist Begruendung bei Abweichung.
        n_idx = col_indices["Wiki Absatz"]
        v_idx = col_indices["Begruendung bei Abweichung"]
        # Erstellen Sie den Bereichsnamen (z.B. "N:V")
        n_letter = _get_col_letter(n_idx + 1)
        v_letter = _get_col_letter(v_idx + 1)
        nv_range_letter = f'{n_letter}:{v_letter}'  # z.B. N:V
        # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
        # Anzahl der Spalten = V_Index - N_Index + 1
        empty_nv_values = [''] * (v_idx - n_idx + 1)

        # Timestamps AN, AO, AP, AX leeren.
        an_letter = _get_col_letter(
            col_indices["Wikipedia Timestamp"] + 1)  # AN (Wiki Extraction TS)
        ao_letter = _get_col_letter(
            col_indices["Timestamp letzte Pruefung"] +
            1)  # AO (Chat Evaluation TS)
        ap_letter = _get_col_letter(
            col_indices["Version"] + 1)  # AP (Version)
        ax_letter = _get_col_letter(
            col_indices["Wiki Verif. Timestamp"] + 1)  # AX (Wiki Verif. TS)

        # --- Verarbeitung ---
        # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1)
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
        all_sheet_updates = []

        # Zaehlt Zeilen, fuer die SerpAPI versucht wurde (im Rahmen des Limits
        # zaehlen).
        processed_count = 0
        # Zaehlt Zeilen, die uebersprungen wurden (verschiedene Gruende).
        skipped_count = 0
        # Zaehlt Zeilen, wo eine URL gefunden und eingetragen wurde.
        found_urls_count = 0

        # Aktueller Zeitstempel fuer den AY Timestamp
        now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).")
                # # Zu viel Laerm im Debug
                skipped_count += 1  # Zaehlen als uebersprungen
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: SerpAPI Wiki Search Timestamp (AY) ist leer.
            # UND Wiki URL (M) ist leer oder "k.A.".
            # UND (Umsatz CRM (J) > min_umsatz MIO € ODER Mitarbeiter CRM (K) >
            # min_employees).

            # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne
            # Helfer _get_cell_value_safe)
            ay_value = self._get_cell_value_safe(
                row, "SerpAPI Wiki Search Timestamp").strip()  # Block 1 Column Map
            m_value = self._get_cell_value_safe(
                row, "Wiki URL").strip()  # Block 1 Column Map
            umsatz_val_str = self._get_cell_value_safe(
                row, "CRM Umsatz")  # Block 1 Column Map
            ma_val_str = self._get_cell_value_safe(
                row, "CRM Anzahl Mitarbeiter")  # Block 1 Column Map

            # Pruefen Sie, ob AY leer ist.
            is_ay_empty = not ay_value
            # Pruefen Sie, ob M leer oder "k.A." ist.
            is_m_empty_or_ka = not m_value or (isinstance(
                m_value, str) and m_value.lower() == "k.a.")

            # Nutze die globale Hilfsfunktion (Block 5), um die Werte fuer den Groessen-Filter zu bekommen.
            # get_numeric_filter_value gibt 0 fuer ungueltige/leere Werte
            # zurueck.
            umsatz_val_mio = get_numeric_filter_value(
                umsatz_val_str, is_umsatz=True)
            ma_val_num = get_numeric_filter_value(ma_val_str, is_umsatz=False)

            # Pruefen Sie, ob das Groessen-Kriterium erfuellt ist.
            size_criteria_met = (
                umsatz_val_mio > min_umsatz) or (
                ma_val_num > min_employees)

            # Verarbeitung ist noetig, wenn AY leer ist UND M leer/k.A. ist UND
            # das Groessen-Kriterium erfuellt ist.
            processing_needed_for_row = is_ay_empty and is_m_empty_or_ka and size_criteria_met

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                company_name = self._get_cell_value_safe(
                    row, "CRM Name").strip()  # Block 1 Column Map
                self.logger.debug(
                    f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Fuehre SerpAPI Suche aus ---
            # Zaehle die Zeile, fuer die SerpAPI versucht wird (im Rahmen des
            # Limits zaehlen)
            processed_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            # Hole Firmenname und Website fuer die Suche (nutzt interne Helfer
            # _get_cell_value_safe)
            company_name = self._get_cell_value_safe(
                row, "CRM Name").strip()  # Block 1 Column Map
            # Block 1 Column Map (Website kann fuer SerpAPI Kontext hilfreich
            # sein)
            website_url = self._get_cell_value_safe(row, "CRM Website").strip()

            # Wenn kein Firmenname vorhanden ist, kann die Suche nicht
            # durchgefuehrt werden
            if not company_name:
                self.logger.warning(
                    f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).")  # <<< GEÄNDERT
                skipped_count += 1  # Zaehlen als uebersprungene Zeile, da Suche nicht moeglich
                # Setze AY Timestamp auch hier, um nicht immer wieder zu versuchen
                # Erstelle leeres Update-Dict, damit extend funktioniert
                updates = []
                updates.append({'range': f'{ts_ay_letter}{i}', 'values': [
                               [now_timestamp_str]]})  # Block 1 Column Map
                # Fuege dieses einzelne Update zur Liste hinzu
                all_sheet_updates.extend(updates)
                updates = []  # Leere die lokale Liste
                continue  # Springe zur naechsten Zeile

            self.logger.info(
                f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...")  # <<< GEÄNDERT

            # Führe die SerpAPI Suche durch (nutzt globale Funktion Block 10 mit Retry).
            # serp_wikipedia_lookup ist mit retry_on_failure dekoriert (Block 2).
            # Wenn serp_wikipedia_lookup nach Retries fehlschlaegt, wirft er
            # eine Exception.
            wiki_url_found = None  # Initialisiere mit None
            try:
                wiki_url_found = serp_wikipedia_lookup(
                    company_name, website=website_url)  # Nutzt globalen Helfer (Block 10)
                # Wenn serp_wikipedia_lookup erfolgreich ist, gibt es die URL
                # oder None zurueck.

            except Exception as e_serp_wiki:
                # Wenn serp_wikipedia_lookup eine Exception wirft (nach
                # Retries)
                self.logger.error(
                    f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}")  # <<< GEÄNDERT
                # wiki_url_found bleibt None. Fahren Sie fort.
                pass  # Fahren Sie fort, um Timestamp zu setzen und Updates vorzubereiten

            # --- Updates vorbereiten ---
            # Timestamp AY IMMER setzen, nachdem der Versuch gemacht wurde,
            # unabhaengig vom Ergebnis der Suche.
            updates_for_row = []  # Lokale Liste fuer Updates dieser Zeile

            updates_for_row.append({'range': f'{ts_ay_letter}{i}', 'values': [
                                   [now_timestamp_str]]})  # Block 1 Column Map

            # Wenn eine URL gefunden wurde, bereite weitere Updates vor.
            # Eine gefundene URL ist ein String, der nicht None ist und nicht
            # "k.A." oder Fehlerstring ist.
            if wiki_url_found and isinstance(
                    wiki_url_found,
                    str) and wiki_url_found.lower() not in [
                    "k.a.",
                    "kein artikel gefunden"] and not wiki_url_found.startswith("FEHLER"):  # Korrektur Pruefung
                self.logger.info(
                    f"  -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).")  # <<< GEÄNDERT
                found_urls_count += 1  # Zaehle den Fund

                # Setze M (Wiki URL) mit der gefundenen URL
                updates_for_row.append({'range': f'{m_letter}{i}', 'values': [
                                       [wiki_url_found]]})  # Block 1 Column Map
                # Setze ReEval Flag (A) auf 'x' (signalisiert, dass eine
                # Re-Evaluation noetig ist)
                updates_for_row.append({'range': f'{a_letter}{i}', 'values': [
                                       ['x']]})  # Block 1 Column Map

                # Leere Spalten N-V.
                # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der
                # Bereichsname ermittelt werden konnte.
                if nv_range_letter:  # Pruefe, ob der Bereichsname ermittelt werden konnte.
                    updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [
                                           empty_nv_values]})  # Block 1 Column Map, lokale Variable
                else:
                    self.logger.warning(
                        f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.")  # <<< GEÄNDERT

                # Leere Timestamps AN, AO, AP, AX.
                # Dies setzt die Zeile zurueck, damit andere Schritte sie
                # spaeter bearbeiten.
                # Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{an_letter}{i}', 'values': [['']]})
                # Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ao_letter}{i}', 'values': [['']]})
                # Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ap_letter}{i}', 'values': [['']]})
                # Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ax_letter}{i}', 'values': [['']]})

            else:
                # Wenn keine Wiki-URL ueber SerpAPI gefunden wurde
                self.logger.debug(
                    f"  -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.")  # <<< GEÄNDERT
                # Nur AY Timestamp wird gesetzt, was bereits oben passiert ist.
                # Keine weiteren Updates fuer M, A, N-V etc.

            # Sammle die Updates fuer diese Zeile in der globalen Liste.
            all_sheet_updates.extend(updates_for_row)

            # Sende gesammelte Sheet Updates, wenn das Update-Batch-Limit erreicht ist.
            # update_batch_row_limit wird aus Config geholt (Block 1).
            # Die Anzahl der Updates pro Zeile variiert (1 bei nicht gefunden, ca. 10+ bei gefunden).
            # Pruefen Sie einfach die Laenge der gesammelten Liste.
            if len(all_sheet_updates) >= update_batch_row_limit * \
                    5:  # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile
                self.logger.debug(
                    f"  Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
                # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
                # Wenn es fehlschlaegt, wird es intern geloggt.
                success = self.sheet_handler.batch_update_cells(
                    all_sheet_updates)
                if success:
                    self.logger.info(
                        f"  Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")  # <<< GEÄNDERT
                # Der Fehlerfall wird von batch_update_cells geloggt

                # Leere die gesammelten Updates nach dem Senden.
                all_sheet_updates = []

            # Kleine Pause nach jeder SerpAPI-Suche (nutzt Config Block 1).
            # Der retry_on_failure Decorator (Block 2) kuemmert sich um Retries mit Backoff.
            # Dies ist eine globale Rate-Limit-Vorsorge zwischen einzelnen
            # Anfragen.
            serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5)
            # self.logger.debug(f"Warte {serp_delay:.2f}s nach SerpAPI
            # Suche...") # Zu viel Laerm im Debug
            time.sleep(serp_delay)

        # --- Finale Sheet Updates senden ---
        # Sende alle verbleibenden gesammelten Updates in einem letzten
        # Batch-Update.
        if all_sheet_updates:
            self.logger.info(
                f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
            # Nutzt die batch_update_cells Methode des Sheet Handlers (Block
            # 14) mit Retry.
            success = self.sheet_handler.batch_update_cells(all_sheet_updates)
            if success:
                # <<< GEÄNDERT
                self.logger.info(f"FINALES Sheet-Update erfolgreich.")
            # Der Fehlerfall wird von batch_update_cells geloggt

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.")  # <<< GEÄNDERT

    def process_contact_search(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Sucht LinkedIn Kontakte ueber SerpAPI.
        """
        self.logger.info(
            f"Starte Contact Research (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        """
        Sucht LinkedIn Kontakte ueber SerpAPI fuer Zeilen, bei denen der
        Contact Search Timestamp (AM) leer ist. Traegt Trefferzahlen in
        AI-AL und den Timestamp in AM ein. Schreibt Details optional in ein 'Contacts' Blatt.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AM).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge die Konfiguration des Batch-Laufs
        self.logger.info(
            f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT

        # --- Daten laden und Startzeile ermitteln ---
        # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
        if start_sheet_row is None:
            self.logger.info(
                "Automatische Ermittlung der Startzeile basierend auf leeren AM...")  # <<< GEÄNDERT
            # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AM (Block 1 Column Map).
            # Standardmaessig ab Zeile 7
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key="Contact Search Timestamp", min_sheet_row=7)

            # Wenn get_start_row_index -1 zurueckgibt (Fehler)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")  # <<< GEÄNDERT
                return  # Beende die Methode

            # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten
            # Daten-Index
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1  # Block 14 SheetHandler Attribut
            self.logger.info(
                f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}")  # <<< GEÄNDERT
        else:
            # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
            # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block
            # 2).
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "FEHLER beim Laden der Daten fuer process_contact_search.")  # <<< GEÄNDERT
                return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den verarbeitungsbereich
        self.logger.info(
            f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht
        # ueber Gesamtzeilen)
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes und Buchstaben ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = [
            "Contact Search Timestamp",  # AM - Pruefkriterium / Timestamp
            # B, C, D (Daten fuer Suche)
            "CRM Name", "CRM Kurzform", "CRM Website",
            # AI, AJ (Zielspalten fuer Trefferzahlen)
            "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden",
            # AK, AL (Zielspalten fuer Trefferzahlen)
            "Linked Management gefunden", "Linked Disponent gefunden"
        ]
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Spaltenbuchstaben fuer Updates (AI-AL, AM) (nutzt
        # interne Helfer _get_col_letter Block 14)
        ts_am_letter = _get_col_letter(
            col_indices["Contact Search Timestamp"] + 1)  # AM
        ai_letter = _get_col_letter(
            col_indices["Linked Serviceleiter gefunden"] + 1)  # AI
        aj_letter = _get_col_letter(
            col_indices["Linked It-Leiter gefunden"] + 1)  # AJ
        ak_letter = _get_col_letter(
            col_indices["Linked Management gefunden"] + 1)  # AK
        al_letter = _get_col_letter(
            col_indices["Linked Disponent gefunden"] + 1)  # AL

        # Positionen, nach denen gesucht wird (kann in Config verschoben werden Block 1)
        # Die Zuordnung zur Zaehlspalte (AI-AL) muss hier im Code erfolgen.
        positions_to_search = {
            "Serviceleiter": ["Serviceleiter", "Leiter Kundendienst", "Einsatzleiter"],
            "IT-Leiter": ["IT-Leiter", "Leiter IT"],
            # Management erweitert
            "Management": ["Geschäftsführer", "Vorstand", "Inhaber", "CEO", "CTO", "COO", "Kaufmännischer Leiter", "Technischer Leiter"],
            "Disponent": ["Disponent", "Einsatzplaner"]  # Disponent erweitert
        }
        # Stellen Sie sicher, dass die Schluessel im Dict den COLUMN_MAP Keys (AI-AL) entsprechen,
        # damit die Zaehlung korrekt zugeordnet werden kann.

        # --- Kontakte-Blatt oeffnen oder erstellen ---
        contacts_sheet = None  # Initialisiere mit None
        # Der Zugriff auf das Spreadsheet-Objekt erfolgt ueber den SheetHandler
        # (Block 14).
        if self.sheet_handler and self.sheet_handler.sheet and self.sheet_handler.sheet.spreadsheet:
            try:
                # Versuche, das Sheet "Contacts" zu oeffnen
                contacts_sheet = self.sheet_handler.sheet.spreadsheet.worksheet(
                    "Contacts")
                self.logger.info("Blatt 'Contacts' gefunden.")  # <<< GEÄNDERT
            except gspread.exceptions.WorksheetNotFound:
                # Wenn nicht gefunden, erstelle es.
                # <<< GEÄNDERT
                self.logger.info(
                    "Blatt 'Contacts' nicht gefunden, erstelle neu...")
                try:
                    # Definieren Sie den Header fuer das neue Blatt
                    contacts_header = [
                        "Firmenname",
                        "CRM Kurzform",
                        "Website",
                        "Geschlecht",
                        "Vorname",
                        "Nachname",
                        "Position",
                        "Suchbegriffskategorie",
                        "E-Mail-Adresse",
                        "LinkedIn-Link",
                        "Timestamp"]
                    # Schaetzen Sie die Anzahl der Zeilen und Spalten fuer das
                    # neue Blatt (kann angepasst werden)
                    num_cols_contacts_sheet = len(contacts_header)
                    # Erstellen Sie das neue Blatt
                    contacts_sheet = self.sheet_handler.sheet.spreadsheet.add_worksheet(
                        title="Contacts", rows="5000", cols=num_cols_contacts_sheet)
                    # Schreiben Sie den Header in die erste Zeile des neuen Blattes
                    # Nutzt _get_col_letter interne Methode des SheetHandlers
                    # (Block 14)
                    contacts_sheet.update(
                        values=[contacts_header],
                        range_name=f"A1:{_get_col_letter(num_cols_contacts_sheet)}1")
                    # <<< GEÄNDERT
                    self.logger.info(
                        "Neues Blatt 'Contacts' erstellt und Header eingetragen.")

                except Exception as e_create_sheet:
                    # Fange Fehler bei der Erstellung des Blattes ab und logge
                    # sie.
                    self.logger.critical(
                        f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.")  # <<< GEÄNDERT
                    # Logge den Traceback.
                    self.logger.debug(traceback.format_exc())  # <<< GEÄNDERT
                    # Setze contacts_sheet auf None, um spaetere
                    # Schreibversuche zu verhindern
                    contacts_sheet = None

        else:
            # Wenn SheetHandler oder Sheet-Objekt nicht verfuegbar war.
            self.logger.warning(
                "SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.")  # <<< GEÄNDERT
            contacts_sheet = None  # Sicherstellen, dass contacts_sheet None ist

        # --- Verarbeitung ---
        # Gesammelte Updates fuer Batch-Schreiben ins Hauptblatt (Liste von
        # Dicts)
        all_sheet_updates = []
        # Gesammelte Zeilen fuer append_rows ins Contacts-Blatt (Liste von
        # Listen)
        all_contact_rows_to_append = []
        # append_rows kann grosse Batches handhaben, wir koennen hier mehr sammeln als beim Batch-Update.
        # Oder wir schreiben pro Firma in das Contacts-Blatt (weniger sammelbar).
        # Fuer diesen Modus sammeln wir alle Kontaktzeilen und schreiben am
        # Ende gesammelt mit append_rows.

        # Zaehlt Zeilen im Hauptblatt, die fuer die Verarbeitung in Frage
        # kommen und in den Batch aufgenommen werden (im Rahmen des Limits).
        processed_count = 0
        # Zaehlt Zeilen im Hauptblatt, die uebersprungen wurden (wegen AM oder
        # fehlender Daten).
        skipped_count = 0

        # Aktueller Zeitstempel fuer die AM Timestamp und Kontaktzeilen
        now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).")
                # # Zu viel Laerm im Debug
                skipped_count += 1  # Zaehlen als uebersprungen
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: Contact Search Timestamp (AM) ist leer.
            # ZUSAETZLICH: Pruefen, ob CRM Name, Kurzform und Website vorhanden
            # und gueltig sind.

            # Holen Sie den Wert aus Spalte AM (Contact Search Timestamp)
            # (nutzt interne Helfer _get_cell_value_safe)
            am_value = self._get_cell_value_safe(
                row, "Contact Search Timestamp").strip()  # Block 1 Column Map
            # Pruefung basiert darauf, ob AM leer ist.
            processing_needed_based_on_status = not am_value

            # Holen Sie die benoetigten Daten fuer die Suche (nutzt interne
            # Helfer _get_cell_value_safe)
            company_name = self._get_cell_value_safe(
                row, "CRM Name").strip()  # Block 1 Column Map
            crm_kurzform = self._get_cell_value_safe(
                row, "CRM Kurzform").strip()  # Block 1 Column Map
            website = self._get_cell_value_safe(
                row, "CRM Website").strip()  # Block 1 Column Map

            # Pruefen Sie, ob die Mindestdaten fuer die Suche vorhanden und gueltig sind.
            # Name und Kurzform duerfen nicht leer sein. Website muss vorhanden
            # und gueltig aussehen.
            has_min_data_for_search = company_name and crm_kurzform and website and isinstance(
                website, str) and website.lower() not in [
                "k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]  # Fuege "http:" hinzu

            # Verarbeitung ist noetig, wenn AM leer ist UND die Mindestdaten
            # fuer die Suche vorhanden sind.
            processing_needed_for_row = processing_needed_based_on_status and has_min_data_for_search

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                # Gekuerzt loggen
                company_name_log = company_name[:50] + \
                    '...' if len(company_name) > 50 else company_name
                self.logger.debug(
                    f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Fuehre LinkedIn Suche(n) aus ---
            # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im
            # Rahmen des Limits zaehlen)
            processed_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            self.logger.info(
                f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...")  # <<< GEÄNDERT

            # Liste zum Sammeln aller gefundenen Kontakte fuer DIESE Zeile
            # (Liste von Dicts)
            all_found_contacts_for_row = []
            # Dictionary zum Zaehlen der Treffer pro Kategorie fuer diese Zeile
            # (AI-AL)
            contact_counts_for_row = {
                key: 0 for key in positions_to_search.keys()}

            # Führe die Suche fuer jede Positionskategorie durch.
            # positions_to_search Dictionary ist oben definiert.
            for category, queries in positions_to_search.items():
                # Führe die Suche fuer jede spezifische Abfrage innerhalb der Kategorie durch.
                # search_linkedin_contacts (Block 10) nutzt den retry_on_failure Decorator (Block 2).
                # Wenn search_linkedin_contacts fehlschlaegt, wirft es eine
                # Exception oder gibt eine leere Liste zurueck.
                # Dictionary zum Sammeln eindeutiger Kontakte {linkedin_url:
                # contact_data} fuer diese Kategorie
                found_contacts_in_category = {}

                for position_query in queries:
                    self.logger.debug(
                        f"  -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...")  # <<< GEÄNDERT
                    try:
                        # Rufe die globale Funktion search_linkedin_contacts auf (Block 10).
                        # Limitieren Sie die Anzahl der SerpAPI Ergebnisse pro
                        # Query, um Kosten zu managen.
                        contacts_from_query = search_linkedin_contacts(
                            company_name=company_name,
                            # Voller Name fuer Kontext (optional genutzt)
                            website=website,  # Website fuer Email-Generierung spaeter
                            position_query=position_query,  # Die spezifische Position
                            crm_kurzform=crm_kurzform,  # Die Kurzform der Firma
                            num_results=getattr(
                                Config,
                                'SERPAPI_LINKEDIN_RESULTS_PER_QUERY',
                                5)  # Konfigurierbar in Config (Block 1)
                        )

                        # Fuege die gefundenen Kontakte (mit Suchkategorie) zur
                        # Liste fuer diese Kategorie hinzu, dedupliziert ueber
                        # URL.
                        for contact in contacts_from_query:
                            linkedin_url = contact.get("LinkedInURL")
                            if linkedin_url and isinstance(
                                    linkedin_url, str) and linkedin_url.strip():  # Stelle sicher, dass URL gueltig ist
                                if linkedin_url not in found_contacts_in_category:
                                    # Wenn die URL noch nicht in dieser
                                    # Kategorie gefunden wurde, fuege den
                                    # Kontakt hinzu.
                                    # Speichere die Kategorie, die den Treffer
                                    # brachte
                                    contact["Suchbegriffskategorie"] = category
                                    found_contacts_in_category[linkedin_url] = contact
                                # else: Wenn die URL bereits gefunden wurde, mache nichts (erste Kategorie wird beibehalten).
                                # self.logger.debug(f"    -> Gefunden:
                                # {contact.get('Vorname')}
                                # {contact.get('Nachname')}
                                # ({contact.get('Position')})") # Zu viel Laerm
                                # im Debug

                    except Exception as e_linkedin_search:
                        # Wenn search_linkedin_contacts eine Exception wirft (nach Retries)
                        # Der Fehler wird bereits vom retry_on_failure
                        # Decorator oder search_linkedin_contacts geloggt.
                        self.logger.error(
                            f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}")  # <<< GEÄNDERT
                        pass  # Faert fort mit der naechsten Query oder Kategorie

                    # Pause nach jeder SerpAPI Suche (pro position_query)
                    # Nutzt Config.SERPAPI_DELAY (Block 1).
                    serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5)
                    # self.logger.debug(f"Warte {serp_delay:.2f}s nach LinkedIn
                    # Suche fuer '{position_query}'...") # Zu viel Laerm im
                    # Debug
                    time.sleep(serp_delay)

                # Zaehle die eindeutigen Treffer in dieser Kategorie nach allen
                # Queries innerhalb der Kategorie.
                contact_counts_for_row[category] = len(
                    found_contacts_in_category)
                # Fuege die eindeutigen Kontakte DIESER Kategorie zur
                # Gesamtliste fuer DIESE Zeile hinzu.
                all_found_contacts_for_row.extend(
                    found_contacts_in_category.values())

            # --- Verarbeite gefundene Kontakte und bereite Updates vor ---
            # Liste von Listen fuer append_rows ins 'Contacts' Blatt
            rows_to_append_to_contacts_sheet = []
            # Updates fuer das Hauptblatt (AI-AL, AM) fuer DIESE Zeile
            main_sheet_updates_for_row = []
            # Timestamp fuer DIESE Zeile/Kontakte
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            # Fuegen Sie die Updates fuer die Trefferzahlen im Hauptblatt hinzu (nutzt interne Helfer _get_col_letter Block 14)
            # Stellen Sie sicher, dass die Spaltenbuchstaben korrekt sind (AI,
            # AJ, AK, AL) (aus oben ermittelt)
            main_sheet_updates_for_row.append({'range': f'{ai_letter}{i}', 'values': [
                                              [str(contact_counts_for_row.get("Serviceleiter", 0))]]})
            main_sheet_updates_for_row.append({'range': f'{aj_letter}{i}', 'values': [
                                              [str(contact_counts_for_row.get("IT-Leiter", 0))]]})
            main_sheet_updates_for_row.append({'range': f'{ak_letter}{i}', 'values': [
                                              [str(contact_counts_for_row.get("Management", 0))]]})
            main_sheet_updates_for_row.append({'range': f'{al_letter}{i}', 'values': [
                                              [str(contact_counts_for_row.get("Disponent", 0))]]})
            # Setze den Contact Search Timestamp (AM) fuer DIESE Zeile
            main_sheet_updates_for_row.append(
                {'range': f'{ts_am_letter}{i}', 'values': [[timestamp]]})

            # Sammeln Sie diese Updates fuer das Hauptblatt in der globalen
            # Liste all_sheet_updates.
            all_sheet_updates.extend(main_sheet_updates_for_row)
            self.logger.info(
                f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.")  # <<< GEÄNDERT

            # Bereiten Sie die Zeilen fuer das 'Contacts' Blatt vor (falls es existiert).
            # all_found_contacts_for_row enthaelt alle gefundenen Kontakte fuer
            # DIESE Zeile (mit evtl. Duplikaten bei URL).
            # Pruefen Sie, ob das Contacts-Sheet geoeffnet/erstellt werden
            # konnte (siehe Initialisierung oben)
            if contacts_sheet:
                # Führen Sie eine finale Deduplizierung ueber die LinkedIn-URL durch.
                # Dictionary-Comprehension behält nur das letzte Vorkommen bei
                # Duplikaten.
                unique_contacts_for_row_dict = {c['LinkedInURL']: c for c in all_found_contacts_for_row if c.get(
                    'LinkedInURL')}  # Filtere Kontakte ohne URL
                unique_contacts_for_row = list(
                    unique_contacts_for_row_dict.values())  # Liste der eindeutigen Kontakte

                # Iteriere ueber die eindeutigen Kontakte fuer diese Zeile
                for contact in unique_contacts_for_row:
                    # Nutzt den extrahierten Vornamen
                    firstname = contact.get("Vorname", "")
                    # Nutzt den extrahierten Nachnamen
                    lastname = contact.get("Nachname", "")

                    # Generiere Geschlecht und E-Mail-Adresse (nutzt globale Funktionen Block 5).
                    # get_gender und get_email_address behandeln
                    # leere/ungueltige Eingaben.
                    gender_value = get_gender(firstname)
                    # Nutzt die Website der Firma (initial geladen)
                    email = get_email_address(firstname, lastname, website)

                    # Erstellen Sie die Liste der Werte fuer eine Zeile im
                    # 'Contacts' Blatt.
                    contact_row = [
                        contact.get("Firmenname", ""),  # Voller Firmenname
                        contact.get("CRM Kurzform", ""),  # Firmenkurzform
                        contact.get("Website", ""),  # Website der Firma
                        gender_value,  # Generiertes Geschlecht
                        firstname,  # Extrahierter Vorname
                        lastname,  # Extrahierter Nachname
                        # Extrahierte oder Fallback Position
                        contact.get("Position", ""),
                        # Kategorie, die den Treffer brachte
                        contact.get("Suchbegriffskategorie", ""),
                        email,  # Generierte E-Mail-Adresse
                        # URL des LinkedIn Profils
                        contact.get("LinkedInURL", ""),
                        timestamp  # Zeitstempel des Suchlaufs
                    ]
                    # Fuegen Sie diese Zeile zur Liste der Zeilen hinzu, die
                    # spaeter ins Contacts-Sheet geschrieben werden.
                    rows_to_append_to_contacts_sheet.append(contact_row)

                # Wenn Zeilen zum Anfuegen gefunden wurden
                if rows_to_append_to_contacts_sheet:
                    # Fuegen Sie diese Zeilen zur globalen Liste aller Kontakte
                    # hinzu, die spaeter angefuegt werden.
                    all_contact_rows_to_append.extend(
                        rows_to_append_to_contacts_sheet)
                    self.logger.debug(
                        f"  -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.")  # <<< GEÄNDERT
                else:
                    self.logger.debug(
                        f"  -> Keine neuen Kontakte fuer Zeile {i} gefunden.")  # <<< GEÄNDERT

            # Sende gesammelte Sheet Updates (Hauptblatt) wenn das Update-Batch-Limit erreicht ist.
            # update_batch_row_limit wird aus Config geholt (Block 1).
            # Updates pro Zeile im Hauptblatt sind 5 (AI-AL + AM). Anzahl der
            # Zeilen = len(all_sheet_updates) / 5.
            rows_in_main_sheet_update_batch = len(all_sheet_updates) // 5

            if rows_in_main_sheet_update_batch >= update_batch_row_limit:
                self.logger.debug(
                    f"  Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
                # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
                # Wenn es fehlschlaegt, wird es intern geloggt.
                success = self.sheet_handler.batch_update_cells(
                    all_sheet_updates)
                if success:
                    self.logger.info(
                        f"  Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.")  # <<< GEÄNDERT
                # Der Fehlerfall wird von batch_update_cells geloggt

                # Leere die gesammelten Updates nach dem Senden.
                all_sheet_updates = []

            # Eine laengere Pause nach der Verarbeitung jeder Firma im Contact Search Modus.
            # Dieser Modus ist API-intensiv und sollte langsamer laufen.
            # Nutzt Config.RETRY_DELAY (Block 1).
            # Laengere Pause, z.B. 80% der Retry-Wartezeit
            pause_duration = getattr(Config, 'RETRY_DELAY', 10) * 0.8
            self.logger.debug(
                f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...")  # <<< GEÄNDERT
            time.sleep(pause_duration)

        # --- Finale Sheet Updates (Hauptblatt) senden ---
        # Sende alle verbleibenden gesammelten Updates in einem letzten
        # Batch-Update.
        if all_sheet_updates:
            rows_in_final_main_sheet_update_batch = len(all_sheet_updates) // 5
            self.logger.info(
                f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
            # Nutzt die batch_update_cells Methode des Sheet Handlers (Block
            # 14) mit Retry.
            success = self.sheet_handler.batch_update_cells(all_sheet_updates)
            if success:
                # <<< GEÄNDERT
                self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.")
            # Der Fehlerfall wird von batch_update_cells geloggt

        # --- Finale Kontakte-Zeilen (Contacts Sheet) anfuegen ---
        # Fuege alle gesammelten Kontaktzeilen auf einmal ans Ende des
        # 'Contacts' Blattes an.
        if contacts_sheet and all_contact_rows_to_append:
            self.logger.info(
                f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...")  # <<< GEÄNDERT
            try:
                # append_rows ist effizienter als batch_update fuer viele neue Zeilen am Ende.
                # Die gspread.Worksheet.append_rows Methode kann Exceptions werfen (z.B. APIError),
                # die hier gefangen werden koennen, wenn gewuenscht.
                # Wenn sie eine Exception wirft, wird diese nicht von retry_on_failure auf
                # process_contact_search behandelt, da append_rows nicht mit @retry_on_failure
                # dekoriert ist. Sie muessten append_rows selbst in einen try/except Block packen oder
                # es mit @retry_on_failure dekorieren (falls gspread es unterstuetzt).
                # Fuer jetzt, fangen wir die Exception hier.
                contacts_sheet.append_rows(
                    all_contact_rows_to_append,
                    value_input_option='USER_ENTERED')  # Standard Option
                self.logger.info(
                    f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.")  # <<< GEÄNDERT
            except Exception as e_append:
                # Fange Fehler beim Anfuegen der Zeilen ab und logge sie.
                self.logger.error(
                    f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}")  # <<< GEÄNDERT
                # Logge den Traceback.
                self.logger.debug(traceback.format_exc())  # <<< GEÄNDERT
                pass  # Faert fort, der Rest des Skripts sollte nicht blockiert werden

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")  # <<< GEÄNDERT

    def process_url_check(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Sucht nach Zeilen mit URL_CHECK_MARKER und versucht, eine neue URL zu finden.
        """
        self.logger.info(
            f"Starte Modus 'check_urls'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        """
        Sucht nach Zeilen, die in Spalte AR mit URL_CHECK_MARKER oder bekannten "k.A. (Fehler...)"
        Mustern markiert sind UND bei denen der AY-Timestamp (SerpAPI Wiki Search Timestamp) leer ist
        (außer bei URL_CHECK_MARKER, der immer eine Suche auslöst).
        Versucht, eine neue URL ueber SerpAPI zu finden.
        Wenn erfolgreich und URL ist NEU: Aktualisiert D, loescht AR, setzt ReEval-Flag (A) und loescht Timestamps.
        Wenn URL identisch oder keine neue URL gefunden: AR wird entsprechend aktualisiert.
        Setzt immer den AY-Timestamp (als Timestamp der URL-Prüfung).

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt).
        """
        self.logger.info(
            f"Starte Modus 'check_urls'. Sucht nach '{URL_CHECK_MARKER}' oder 'k.A. (Fehler...)' in AR. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")

        # --- Konfiguration holen ---
        update_batch_row_limit = getattr(
            Config,
            'UPDATE_BATCH_ROW_LIMIT',
            50)  # <<< NEUE ZEILE HINZUGEFÜGT

        if not self.sheet_handler.load_data():
            self.logger.error("Fehler beim Laden der Daten fuer URL Check.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)

        if start_sheet_row is None:
            start_sheet_row = header_rows + 1
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows

        self.logger.info(
            f"Suchbereich fuer URL Checks: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.")
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
            return

        required_keys = [
            "Website Rohtext",
            "CRM Name",
            "CRM Website",
            "ReEval Flag",
            "Website Scrape Timestamp",
            "Timestamp letzte Pruefung",
            "Wikipedia Timestamp",
            "Wiki Verif. Timestamp",
            "SerpAPI Wiki Search Timestamp",
            "Version"]
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_url_check: {missing}. Breche ab.")
            return

        ar_letter = _get_col_letter(
            col_indices["Website Rohtext"] + 1)
        d_letter = _get_col_letter(
            col_indices["CRM Website"] + 1)
        a_letter = _get_col_letter(
            col_indices["ReEval Flag"] + 1)
        at_letter = _get_col_letter(
            col_indices["Website Scrape Timestamp"] + 1)
        ao_letter = _get_col_letter(
            col_indices["Timestamp letzte Pruefung"] + 1)
        an_letter = _get_col_letter(
            col_indices["Wikipedia Timestamp"] + 1)
        ax_letter = _get_col_letter(
            col_indices["Wiki Verif. Timestamp"] + 1)
        ay_letter = _get_col_letter(
            col_indices["SerpAPI Wiki Search Timestamp"] +
            1)  # Timestamp dieser Funktion
        ap_letter = _get_col_letter(
            col_indices["Version"] + 1)

        ka_error_patterns = [
            "k.A.",
            "k.A. (Extraktion leer)",
            "k.A. (Nur Cookie-Banner erkannt)",
            "k.A. (Kein Body gefunden)",
            "k.A. (Fehler Parsing:",
            "k.A. (Unerwarteter Fehler Task)",
            "k.A. (Fehler Scraping:",
            "k.A. (Timeout",
            "k.A. (SSL Fehler",
            "k.A. (Connection Error",
            "k.A. (HTTP Error",
            URL_CHECK_MARKER]

        all_sheet_updates = []
        processed_count = 0
        skipped_count = 0
        found_new_url_count = 0
        now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1
            if row_index_in_list >= total_sheet_rows:
                break

            row = all_data[row_index_in_list]
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                skipped_count += 1
                continue

            ar_value = self._get_cell_value_safe(
                row, "Website Rohtext").strip()
            # Verwende den spezifischen Timestamp für diese Funktion
            ay_timestamp_value = self._get_cell_value_safe(
                row, "SerpAPI Wiki Search Timestamp").strip()

            processing_needed_for_row = False
            is_marker_case = ar_value == URL_CHECK_MARKER
            is_ka_error_case = any(
                pattern in ar_value for pattern in ka_error_patterns if pattern != URL_CHECK_MARKER)

            if is_marker_case:  # URL_CHECK_MARKER löst immer eine Suche aus
                processing_needed_for_row = True
            # Alte k.A.-Fehler nur, wenn AY-Timestamp noch nicht gesetzt wurde
            elif is_ka_error_case and not ay_timestamp_value:
                processing_needed_for_row = True

            if not processing_needed_for_row:
                skipped_count += 1
                continue

            processed_count += 1
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_url_check erreicht.")
                break

            company_name = self._get_cell_value_safe(row, "CRM Name").strip()
            old_crm_website_url = self._get_cell_value_safe(
                row, "CRM Website").strip()
            normalized_old_crm_url = simple_normalize_url(old_crm_website_url)

            if not company_name:
                self.logger.warning(
                    f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden).")
                skipped_count += 1
                updates_for_row_skip = [{'range': f'{ay_letter}{i}', 'values': [
                    [now_timestamp_str]]}]  # Timestamp trotzdem setzen
                all_sheet_updates.extend(updates_for_row_skip)
                continue

            self.logger.info(
                f"Zeile {i}: AR='{ar_value[:50]}...'. Suche neue URL für '{company_name[:50]}...' (Aktuell D: '{old_crm_website_url[:50]}...')...")

            updates_for_row = []
            new_url_found_str = "k.A."

            try:
                new_url_found_str = serp_website_lookup(company_name)
                normalized_new_url = simple_normalize_url(new_url_found_str)

                if new_url_found_str != "k.A." and normalized_new_url != "k.A.":
                    if normalized_new_url != normalized_old_crm_url:
                        self.logger.info(
                            f"  -> Neue, andere URL gefunden: {new_url_found_str}. Alte war: '{old_crm_website_url}'. Bereite Update vor.")
                        found_new_url_count += 1
                        updates_for_row.append(
                            {'range': f'{d_letter}{i}', 'values': [[new_url_found_str]]})
                        updates_for_row.append(
                            {'range': f'{ar_letter}{i}', 'values': [['']]})
                        updates_for_row.append(
                            {'range': f'{a_letter}{i}', 'values': [['x']]})
                        updates_for_row.append(
                            {'range': f'{at_letter}{i}', 'values': [['']]})
                        updates_for_row.append(
                            {'range': f'{ao_letter}{i}', 'values': [['']]})
                        updates_for_row.append(
                            {'range': f'{an_letter}{i}', 'values': [['']]})
                        updates_for_row.append(
                            {'range': f'{ax_letter}{i}', 'values': [['']]})
                        # Wird unten explizit neu gesetzt
                        updates_for_row.append(
                            {'range': f'{ay_letter}{i}', 'values': [['']]})
                        updates_for_row.append(
                            {'range': f'{ap_letter}{i}', 'values': [['']]})
                    else:
                        self.logger.info(
                            f"  -> SerpAPI fand URL '{new_url_found_str}', aber diese ist identisch mit der bereits vorhandenen URL in Spalte D. Keine Änderung in D.")
                        updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [
                                               ["k.A. (URL via SerpAPI identisch mit alter URL)"]]})
                else:
                    self.logger.warning(
                        f"  -> Keine neue gueltige URL via SerpAPI für '{company_name[:50]}...' gefunden. Setze AR auf 'k.A. (Keine URL bei Neusuche)'.")
                    updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [
                                           ["k.A. (Keine URL bei Neusuche)"]]})

            except Exception as e_serp_lookup:
                self.logger.error(
                    f"FEHLER bei SERP Website Lookup für Zeile {i} ('{company_name[:50]}...'): {e_serp_lookup}")
                updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [
                                       [f"k.A. (Fehler URL Suche)"]]})
                pass

            updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [
                                   [now_timestamp_str]]})  # AY Timestamp immer setzen
            all_sheet_updates.extend(updates_for_row)

            if len(all_sheet_updates) >= update_batch_row_limit * \
                    3:  # Angepasst, da Anzahl Updates variiert
                self.logger.debug(
                    f"  Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...")
                success = self.sheet_handler.batch_update_cells(
                    all_sheet_updates)
                if success:
                    self.logger.info(
                        f"  Sheet-Update für {len(all_sheet_updates)} Operationen erfolgreich.")
                all_sheet_updates = []

            serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5)
            time.sleep(serp_delay)

        if all_sheet_updates:
            self.logger.info(
                f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...")
            success = self.sheet_handler.batch_update_cells(all_sheet_updates)
            if success:
                self.logger.info(f"FINALES Sheet-Update erfolgreich.")

        self.logger.info(
            f"Modus 'check_urls' abgeschlossen. {processed_count} Zeilen mit Marker/Fehler verarbeitet, {found_new_url_count} neue URLs gefunden, {skipped_count} Zeilen uebersprungen.")

    def process_repair_sitz_data(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Wendet die verbesserte Sitz-Parsing-Logik auf bestehende Daten an.
        """
        self.logger.info(
            f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        """
        Liest bestehende Sitz-Stadt/Land-Angaben, wendet die verbesserte Parsing-Logik
        an und aktualisiert das Sheet, falls sich Änderungen ergeben.
        """
        self.logger.info(
            f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Komplett ab Datenstart'}, End: {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")

        if not self.sheet_handler.load_data():
            self.logger.error(
                "Konnte Sheet-Daten nicht laden für Sitz-Reparatur. Abbruch.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_offset = self.sheet_handler._header_rows

        stadt_col_idx = COLUMN_MAP.get("Wiki Sitz Stadt")
        land_col_idx = COLUMN_MAP.get("Wiki Sitz Land")
        # Optional: Eine Spalte für den originalen Roh-Sitz-String, falls vorhanden
        # roh_sitz_col_idx = COLUMN_MAP.get("IHRE_ROH_SITZ_SPALTE")

        if stadt_col_idx is None or land_col_idx is None:
            self.logger.error(
                "Spaltenindizes für 'Wiki Sitz Stadt' oder 'Wiki Sitz Land' nicht in COLUMN_MAP. Abbruch.")
            return

        updates_fuer_sheet = []
        processed_rows_count = 0
        updated_rows_count = 0

        effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1
        effective_end_row = end_sheet_row if end_sheet_row is not None else len(
            all_data)

        self.logger.info(
            f"Prüfe Zeilen {effective_start_row} bis {effective_end_row} für Sitz-Reparatur.")

        for row_num_sheet in range(effective_start_row, effective_end_row + 1):
            if limit is not None and processed_rows_count >= limit:
                self.logger.info(
                    f"Limit von {limit} erreichten Zeilen für Sitz-Reparatur erreicht.")
                break

            row_list_idx = row_num_sheet - 1
            if row_list_idx >= len(all_data):
                break  # Ende der Daten erreicht

            row_data = all_data[row_list_idx]

            aktuelle_stadt = self._get_cell_value_safe(
                row_data, "Wiki Sitz Stadt")
            aktuelle_land = self._get_cell_value_safe(
                row_data, "Wiki Sitz Land")

            # Erzeuge den Input-String für die Parsing-Funktion
            # Besser: Wenn Sie den *ursprünglichen* String aus der Wikipedia Infobox
            # in einer separaten Spalte gespeichert hätten, würden Sie diesen hier verwenden.
            # Als Fallback kombinieren wir aktuelle Stadt und Land.
            input_sitz_string = aktuelle_stadt
            if aktuelle_land and aktuelle_land.lower() not in ["", "k.a."]:
                if input_sitz_string and input_sitz_string.lower() not in [
                        "", "k.a."]:
                    # Kombiniere mit Komma
                    input_sitz_string += f", {aktuelle_land}"
                else:
                    input_sitz_string = aktuelle_land  # Wenn Stadt leer/kA, nimm nur Land

            if not input_sitz_string or not input_sitz_string.strip(
            ) or input_sitz_string.lower() == 'k.a.':
                # self.logger.debug(f"Zeile {row_num_sheet}: Keine validen aktuellen Sitzdaten ('{aktuelle_stadt}', '{aktuelle_land}') zum Reparieren.")
                continue

            processed_rows_count += 1

            try:
                # Verwende die neue Parsing-Methode des WikipediaScrapers
                # Stellen Sie sicher, dass self.wiki_scraper eine Instanz von
                # WikipediaScraper ist
                parsed_sitz_info = self.wiki_scraper._parse_sitz_string_detailed(
                    input_sitz_string)
                neue_stadt = parsed_sitz_info.get('sitz_stadt', 'k.A.')
                neues_land = parsed_sitz_info.get('sitz_land', 'k.A.')

                # Nur updaten, wenn sich etwas geändert hat
                if (neue_stadt != aktuelle_stadt and not (neue_stadt == "k.A." and aktuelle_stadt == "")) or (
                        neues_land != aktuelle_land and not (neues_land == "k.A." and aktuelle_land == "")):
                    self.logger.info(
                        f"Zeile {row_num_sheet}: SITZ-UPDATE. Input: '{input_sitz_string[:60]}...' Alt: '{aktuelle_stadt} / {aktuelle_land}' -> Neu: '{neue_stadt} / {neues_land}'")
                    updates_fuer_sheet.append({
                        'range': f'{_get_col_letter(stadt_col_idx + 1)}{row_num_sheet}',
                        'values': [[neue_stadt]]
                    })
                    updates_fuer_sheet.append({
                        'range': f'{_get_col_letter(land_col_idx + 1)}{row_num_sheet}',
                        'values': [[neues_land]]
                    })
                    updated_rows_count += 1
                # else:
                    # self.logger.debug(f"Zeile {row_num_sheet}: Keine Änderung bei Sitzdaten für Input '{input_sitz_string[:60]}...'. Alt: '{aktuelle_stadt} / {aktuelle_land}', Neu: '{neue_stadt} / {neues_land}'")

            except Exception as e_parse:
                self.logger.error(
                    f"Fehler beim Parsen des Sitzes für Zeile {row_num_sheet} mit Input '{input_sitz_string}': {e_parse}")

            # Batch-Update Logik
            if len(updates_fuer_sheet) >= getattr(
                    Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 2:  # Mal 2, da zwei Spalten pro Zeile
                self.logger.info(
                    f"Sende Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...")
                self.sheet_handler.batch_update_cells(updates_fuer_sheet)
                updates_fuer_sheet = []
                # time.sleep(1) # Optionale Pause

        # Letzten Batch senden
        if updates_fuer_sheet:
            self.logger.info(
                f"Sende finalen Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...")
            self.sheet_handler.batch_update_cells(updates_fuer_sheet)

        self.logger.info(
            f"Sitz-Daten Reparatur abgeschlossen. {processed_rows_count} Zeilen geprüft, {updated_rows_count} Zeilen aktualisiert.")

    def _get_numeric_value_for_plausi(self, value_str, is_umsatz=False):
        """
        Hilfsfunktion, um numerische Werte für Plausibilitätschecks zu extrahieren.
        """
        # ... (Implementation remains similar to the original code) ...
        # Diese Funktion ist relativ kurz und könnte hier stehen bleiben.
        from helpers import extract_numeric_value
        extracted_val_str = extract_numeric_value(value_str, is_umsatz)
        if extracted_val_str.lower() in ['k.a.', '0']:
            return np.nan
        try:
            num_val = float(extracted_val_str)
            return num_val * 1000000.0 if is_umsatz else num_val
        except (ValueError, TypeError):
            return np.nan

    def _check_financial_plausibility(self, row_data_dict):
        """
        Führt die Plausibilitätschecks für eine Zeile durch.
        """
        results = {
            "plaus_umsatz_flag": "NICHT_PRUEFBAR",
            "plaus_ma_flag": "NICHT_PRUEFBAR",
            "plaus_ratio_flag": "NICHT_PRUEFBAR",
            "abweichung_umsatz_flag": "N/A",
            "abweichung_ma_flag": "N/A",
            "plausi_begruendung_final": "Plausibilität OK"}
        temp_begruendungen = []

        parent_account_name_d_val = row_data_dict.get(
            "Parent Account Name", "").strip()
        is_konzern_tochter_laut_d = bool(
            parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.')

        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ NEU/ERWEITERT: Info aus Spalte O und P für Konzernlogik heranziehen +++
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # Diese Keys müssen im row_data_dict vorhanden sein, wenn diese
        # Funktion aufgerufen wird!
        parent_o_val = row_data_dict.get(
            "System Vorschlag Parent Account",
            "").strip().lower()
        parent_p_val = row_data_dict.get(
            "Parent Vorschlag Status", "").strip().lower()
        is_konzern_tochter_laut_o_und_p = bool(
            parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x')

        # (Das `wiki_stammt_von_parent_explizit` Flag ist optional, wenn Sie den anderen Ansatz verfolgen)
        # wiki_stammt_von_parent_explizit = row_data_dict.get("Wiki Daten von Parent", False)

        # or wiki_stammt_von_parent_explizit
        is_part_of_a_group_for_plausi = is_konzern_tochter_laut_d or is_konzern_tochter_laut_o_und_p

        log_msg_group_parts = []
        if is_konzern_tochter_laut_d:
            log_msg_group_parts.append(f"D='{parent_account_name_d_val}'")
        if is_konzern_tochter_laut_o_und_p:
            log_msg_group_parts.append(f"O/P='{parent_o_val}/{parent_p_val}'")
        # if wiki_stammt_von_parent_explizit:
        #     log_msg_group_parts.append("WikiParentFlag=True")

        if is_part_of_a_group_for_plausi:
            self.logger.debug(
                f"    PlausiCheck: Unternehmen ist Teil einer Gruppe ({'; '.join(log_msg_group_parts)}). Abweichungs-Checks CRM/Wiki werden als INFO_KONZERN_LOGIK behandelt.")
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ ENDE NEU/ERWEITERT ++++++++++++++++++++++++++++++++++++++++++++++
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # --- 1. Plausibilität Finaler Umsatz (BG) ---
        final_umsatz_str = row_data_dict.get(
            "Finaler Umsatz (Wiki>CRM)", "k.A.")
        umsatz_num_absolut = self._get_numeric_value_for_plausi(
            final_umsatz_str, is_umsatz=True)

        exclusion_list_common = ['k.a.', '', 'n/a',
                                 '-', '0', '0.0', '0,00', '0.000', '0.00']
        if pd.isna(umsatz_num_absolut):
            if final_umsatz_str.lower().strip(
            ) not in exclusion_list_common and not final_umsatz_str.startswith("FEHLER"):
                results["plaus_umsatz_flag"] = "FEHLER_FORMAT"
                temp_begruendungen.append(
                    f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden.")
        else:
            results["plaus_umsatz_flag"] = "OK"
            if umsatz_num_absolut == 0 and final_umsatz_str != "0":
                results["plaus_umsatz_flag"] = "WARNUNG_NULL_WERT"
                temp_begruendungen.append(
                    f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').")
            elif umsatz_num_absolut < getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):
                results["plaus_umsatz_flag"] = "WARNUNG_NIEDRIG"
                temp_begruendungen.append(
                    f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) < Min-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):,.0f} €).")
            elif umsatz_num_absolut > getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):
                results["plaus_umsatz_flag"] = "WARNUNG_HOCH"
                temp_begruendungen.append(
                    f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) > Max-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):,.0f} €).")

        # --- 2. Plausibilität Finale Mitarbeiter (BH) ---
        final_ma_str = row_data_dict.get(
            "Finaler Mitarbeiter (Wiki>CRM)", "k.A.")
        ma_num_absolut = self._get_numeric_value_for_plausi(
            final_ma_str, is_umsatz=False)

        if pd.isna(ma_num_absolut):
            if final_ma_str.lower().strip(
            ) not in exclusion_list_common and not final_ma_str.startswith("FEHLER"):
                results["plaus_ma_flag"] = "FEHLER_FORMAT"
                temp_begruendungen.append(
                    f"Finale MA ('{final_ma_str}') konnte nicht als Zahl interpretiert werden.")
        else:
            results["plaus_ma_flag"] = "OK"
            if ma_num_absolut == 0 and final_ma_str != "0":
                results["plaus_ma_flag"] = "WARNUNG_NULL_WERT"
                temp_begruendungen.append(
                    f"Finale MA ist numerisch 0 (aus '{final_ma_str}').")
            elif ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1):
                results["plaus_ma_flag"] = "WARNUNG_NIEDRIG"
                temp_begruendungen.append(
                    f"Finale MA ({ma_num_absolut:.0f}) < Min-Schwelle ({getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1):.0f}).")
            elif not pd.isna(umsatz_num_absolut) and umsatz_num_absolut >= getattr(Config, 'PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK', 1000000) and \
                    ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3):
                results["plaus_ma_flag"] = "WARNUNG_ZU_WENIG_MA_BEI_UMSATZ"
                temp_begruendungen.append(
                    f"Finale MA ({ma_num_absolut:.0f}) auffällig niedrig für Umsatz ({umsatz_num_absolut:,.0f} €).")
            elif ma_num_absolut > getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000):
                results["plaus_ma_flag"] = "WARNUNG_HOCH"
                temp_begruendungen.append(
                    f"Finale MA ({ma_num_absolut:.0f}) > Max-Schwelle ({getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000):,.0f}).")

        # --- 3. Plausibilität Umsatz/MA Ratio (BI) ---
        if not pd.isna(umsatz_num_absolut) and not pd.isna(
                ma_num_absolut) and umsatz_num_absolut > 0:
            if ma_num_absolut > 0:
                ratio = umsatz_num_absolut / ma_num_absolut
                results["plaus_ratio_flag"] = "OK"
                if ratio < getattr(
                    Config,
                    'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN',
                        25000):
                    results["plaus_ratio_flag"] = "WARNUNG_RATIO_NIEDRIG"
                    temp_begruendungen.append(
                        f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) < Min-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000):,.0f} €/MA).")
                elif ratio > getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):
                    results["plaus_ratio_flag"] = "WARNUNG_RATIO_HOCH"
                    temp_begruendungen.append(
                        f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) > Max-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):,.0f} €/MA).")
            elif ma_num_absolut == 0:
                results["plaus_ratio_flag"] = "FEHLER_MA_NULL_BEI_UMSATZ"
                temp_begruendungen.append(
                    "Umsatz vorhanden, aber MA ist 0. Ratio nicht berechenbar.")
        elif pd.isna(umsatz_num_absolut) or umsatz_num_absolut <= 0:
            results["plaus_ratio_flag"] = "NICHT_PRUEFBAR_UMSATZ_FEHLT"

        # --- 4. Abgleich CRM vs. Wiki (BJ, BK) ---
        crm_umsatz_str = row_data_dict.get("CRM Umsatz", "k.A.")
        wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.")
        crm_ma_str = row_data_dict.get("CRM Anzahl Mitarbeiter", "k.A.")
        wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.")

        crm_u_abs = self._get_numeric_value_for_plausi(
            crm_umsatz_str, is_umsatz=True)
        wiki_u_abs = self._get_numeric_value_for_plausi(
            wiki_umsatz_str, is_umsatz=True)
        crm_m_abs_comp = self._get_numeric_value_for_plausi(
            crm_ma_str, is_umsatz=False)
        wiki_m_abs_comp = self._get_numeric_value_for_plausi(
            wiki_ma_str, is_umsatz=False)

        abweichung_prozent_config = getattr(
            Config, 'PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT', 30) / 100.0

        # Umsatz Abweichung (BJ)
        if pd.notna(crm_u_abs) and pd.notna(
                wiki_u_abs) and crm_u_abs > 0 and wiki_u_abs > 0:
            if is_part_of_a_group_for_plausi:  # ERWEITERTE BEDINGUNG HIER VERWENDEN
                results["abweichung_umsatz_flag"] = "INFO_KONZERN_LOGIK"
            else:
                diff_umsatz = abs(crm_u_abs - wiki_u_abs)
                bezugswert_umsatz = max(
                    crm_u_abs, wiki_u_abs) if max(
                    crm_u_abs, wiki_u_abs) > 0 else 1
                if (diff_umsatz / bezugswert_umsatz) > abweichung_prozent_config:
                    results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT"
                    temp_begruendungen.append(
                        f"Umsatz CRM ({crm_u_abs:,.0f} €) vs. Wiki ({wiki_u_abs:,.0f} €) weicht >{abweichung_prozent_config*100:.0f}% ab.")
                else:
                    results["abweichung_umsatz_flag"] = "OK"
        elif pd.notna(crm_u_abs) and crm_u_abs > 0 and (pd.isna(wiki_u_abs) or wiki_u_abs <= 0):
            results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL"
        elif (pd.isna(crm_u_abs) or crm_u_abs <= 0) and pd.notna(wiki_u_abs) and wiki_u_abs > 0:
            results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL"
        else:
            results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL"

        # Mitarbeiter Abweichung (BK)
        if pd.notna(crm_m_abs_comp) and pd.notna(
                wiki_m_abs_comp) and crm_m_abs_comp > 0 and wiki_m_abs_comp > 0:
            if is_part_of_a_group_for_plausi:  # ERWEITERTE BEDINGUNG HIER VERWENDEN
                results["abweichung_ma_flag"] = "INFO_KONZERN_LOGIK"
            else:
                diff_ma = abs(crm_m_abs_comp - wiki_m_abs_comp)
                bezugswert_ma = max(
                    crm_m_abs_comp,
                    wiki_m_abs_comp) if max(
                    crm_m_abs_comp,
                    wiki_m_abs_comp) > 0 else 1
                if (diff_ma / bezugswert_ma) > abweichung_prozent_config:
                    results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT"
                    temp_begruendungen.append(
                        f"MA CRM ({crm_m_abs_comp:.0f}) vs. Wiki ({wiki_m_abs_comp:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.")
                else:
                    results["abweichung_ma_flag"] = "OK"
        elif pd.notna(crm_m_abs_comp) and crm_m_abs_comp > 0 and (pd.isna(wiki_m_abs_comp) or wiki_m_abs_comp <= 0):
            results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL"
        elif (pd.isna(crm_m_abs_comp) or crm_m_abs_comp <= 0) and pd.notna(wiki_m_abs_comp) and wiki_m_abs_comp > 0:
            results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL"
        else:
            results["abweichung_ma_flag"] = "BEIDE_FEHLEN_ODER_NULL"

        if temp_begruendungen:
            results["plausi_begruendung_final"] = "; ".join(temp_begruendungen)

        return results

    def run_plausibility_checks_batch(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Führt Konsolidierung und Plausi-Checks für einen Bereich von Zeilen aus.
        """
        self.logger.info(
            f"Starte Modus 'plausi_check_data'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        self.logger.info(
            f"Starte Modus 'plausi_check_data' (Konsolidierung & Plausi-Checks). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}")

        if not self.sheet_handler.load_data():
            self.logger.error(
                "Konnte Sheet-Daten nicht laden für Plausi-Checks. Abbruch.")
            return

            all_data = self.sheet_handler.get_all_data_with_headers()
            header_offset = self.sheet_handler._header_rows
            total_sheet_rows = len(all_data)

            effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1
            effective_end_row = end_sheet_row if end_sheet_row is not None else total_sheet_rows

            if effective_start_row > effective_end_row or effective_start_row > total_sheet_rows:
                self.logger.info(
                    "Start liegt nach Ende oder außerhalb des Sheets. Keine Zeilen zu verarbeiten.")
                return

            self.logger.info(
                f"Verarbeite Zeilen {effective_start_row} bis {effective_end_row} für Konsolidierung und Plausi-Checks.")

            required_keys_for_plausi_mode = [  # Schlüssel wie gehabt
                "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", "Parent Account Name",
                "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)",
                "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio",
                "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung",
                "Plausibilität Prüfdatum", "CRM Name"  # CRM Name für Logging hinzugefügt
            ]
            if not all(
                    key in COLUMN_MAP for key in required_keys_for_plausi_mode):
                missing_k = [
                    k for k in required_keys_for_plausi_mode if k not in COLUMN_MAP]
                self.logger.error(
                    f"Nicht alle benötigten Spalten ({missing_k}) für Modus 'plausi_check_data' in COLUMN_MAP. Abbruch.")
                return

            all_sheet_updates = []
            processed_rows_count = 0
            now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            update_batch_limit_config = getattr(
                Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

            for row_num_sheet in range(
                    effective_start_row,
                    effective_end_row + 1):
                if limit is not None and processed_rows_count >= limit:
                    self.logger.info(
                        f"Verarbeitungslimit von {limit} Zeilen erreicht.")
                    break

                row_list_idx = row_num_sheet - 1
                if row_list_idx >= total_sheet_rows:
                    break
                row_data = all_data[row_list_idx]

                crm_name_check = self._get_cell_value_safe(
                    row_data, "CRM Name").strip()
                if not crm_name_check:
                    continue

                self.logger.debug(
                    f"Zeile {row_num_sheet} ({crm_name_check[:30]}...): Starte Konsolidierung und Plausi-Check.")
                current_row_updates = []

                # 1. Konsolidierung (BD, BE)
                final_umsatz_str_konsolidiert = "k.A."
                final_ma_str_konsolidiert = "k.A."
                parent_account_name_d_val = self._get_cell_value_safe(
                    row_data, "Parent Account Name").strip()
                parent_o_val_plausi = self._get_cell_value_safe(
                    row_data, "System Vorschlag Parent Account").strip()  # Für Plausi-Check holen
                parent_p_val_plausi = self._get_cell_value_safe(
                    row_data, "Parent Vorschlag Status").strip()       # Für Plausi-Check holen

                try:
                    crm_umsatz_val_str = self._get_cell_value_safe(
                        row_data, "CRM Umsatz")
                    # Wiki-Werte direkt aus der Zeile (row_data) lesen
                    wiki_umsatz_val_str_sheet = self._get_cell_value_safe(
                        row_data, "Wiki Umsatz")
                    crm_ma_val_str = self._get_cell_value_safe(
                        row_data, "CRM Anzahl Mitarbeiter")
                    wiki_ma_val_str_sheet = self._get_cell_value_safe(
                        row_data, "Wiki Mitarbeiter")

                    num_crm_umsatz = get_numeric_filter_value(
                        crm_umsatz_val_str, is_umsatz=True)
                    num_wiki_umsatz = get_numeric_filter_value(
                        wiki_umsatz_val_str_sheet, is_umsatz=True)  # Verwende _sheet Wert
                    num_crm_ma = get_numeric_filter_value(
                        crm_ma_val_str, is_umsatz=False)
                    num_wiki_ma = get_numeric_filter_value(
                        wiki_ma_val_str_sheet, is_umsatz=False)       # Verwende _sheet Wert

                    if parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.':
                        self.logger.debug(
                            f"    -> Parent D ('{parent_account_name_d_val}') ist gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.")
                        final_num_umsatz = num_crm_umsatz if num_crm_umsatz > 0 else num_wiki_umsatz
                        final_num_ma = num_crm_ma if num_crm_ma > 0 else num_wiki_ma
                    else:
                        final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz
                        final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma

                    final_umsatz_str_konsolidiert = str(
                        int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.'
                    final_ma_str_konsolidiert = str(
                        int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.'
                except Exception as e_conso_batch:
                    self.logger.error(
                        f"Fehler bei Konsolidierung in Plausi-Batch für Zeile {row_num_sheet}: {e_conso_batch}")
                    final_umsatz_str_konsolidiert = "FEHLER_KONSO_PLAUSI"
                    final_ma_str_konsolidiert = "FEHLER_KONSO_PLAUSI"

                current_row_updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Umsatz (Wiki>CRM)") + 1)}{row_num_sheet}',
                        'values': [
                            [final_umsatz_str_konsolidiert]]})
                current_row_updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Finaler Mitarbeiter (Wiki>CRM)") + 1)}{row_num_sheet}',
                        'values': [
                            [final_ma_str_konsolidiert]]})

                # 2. Plausibilitäts-Checks (BG-BM)
                if not final_umsatz_str_konsolidiert.startswith(
                        "FEHLER") and not final_ma_str_konsolidiert.startswith("FEHLER"):
                    try:
                        plausi_input_data = {
                            "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert,
                            "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert,
                            "CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"),
                            "Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"),
                            "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"),
                            "Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter"),
                            "Parent Account Name": parent_account_name_d_val,
                            "System Vorschlag Parent Account": parent_o_val_plausi,  # Variable von oben
                            "Parent Vorschlag Status": parent_p_val_plausi        # Variable von oben
                        }
                        plausi_results = self._check_financial_plausibility(
                            plausi_input_data)

                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Plausibilität Umsatz") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "plaus_umsatz_flag",
                                            "ERR_FLAG")]]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Plausibilität Mitarbeiter") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "plaus_ma_flag",
                                            "ERR_FLAG")]]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Plausibilität Umsatz/MA Ratio") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "plaus_ratio_flag",
                                            "ERR_FLAG")]]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Abweichung Umsatz CRM/Wiki") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "abweichung_umsatz_flag",
                                            "ERR_FLAG")]]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Abweichung MA CRM/Wiki") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "abweichung_ma_flag",
                                            "ERR_FLAG")]]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Plausibilität Begründung") + 1)}{row_num_sheet}',
                                'values': [
                                    [
                                        plausi_results.get(
                                            "plausi_begruendung_final",
                                            "Fehler Begr.")]]})

                    except Exception as e_plausi_run_batch:
                        self.logger.error(
                            f"Fehler im Plausi-Check Aufruf (Batch-Modus) für Zeile {row_num_sheet}: {e_plausi_run_batch}")
                        for key_flag in [
                            "Plausibilität Umsatz",
                            "Plausibilität Mitarbeiter",
                            "Plausibilität Umsatz/MA Ratio",
                            "Abweichung Umsatz CRM/Wiki",
                                "Abweichung MA CRM/Wiki"]:
                            current_row_updates.append(
                                {
                                    'range': f'{_get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}',
                                    'values': [
                                        ['FEHLER_CALL']]})
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(get_col_idx("Plausibilität Begründung") + 1)}{row_num_sheet}',
                                'values': [
                                    [f"Systemfehler: {str(e_plausi_run_batch)[:100]}"]]})
                else:  # Fehler bei Konsolidierung
                    self.logger.warning(
                        f"Zeile {row_num_sheet}: Überspringe Plausi-Checks wegen Fehler bei Konsolidierung.")
                    for key_flag in [
                        "Plausibilität Umsatz",
                        "Plausibilität Mitarbeiter",
                        "Plausibilität Umsatz/MA Ratio",
                        "Abweichung Umsatz CRM/Wiki",
                            "Abweichung MA CRM/Wiki"]:
                        current_row_updates.append(
                            {
                                'range': f'{_get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}',
                                'values': [
                                    ['INPUT_FEHLER_KONSO']]})
                    current_row_updates.append(
                        {
                            'range': f'{_get_col_letter(get_col_idx("Plausibilität Begründung") + 1)}{row_num_sheet}',
                            'values': [
                                ["Konsolidierung fehlgeschlagen"]]})

                current_row_updates.append(
                    {
                        'range': f'{_get_col_letter(get_col_idx("Plausibilität Prüfdatum") + 1)}{row_num_sheet}',
                        'values': [
                            [now_timestamp_str]]})

                all_sheet_updates.extend(current_row_updates)
                processed_rows_count += 1

                # Batch-Update auslösen, wenn Limit erreicht
                if processed_rows_count % update_batch_limit_config == 0 and processed_rows_count > 0:
                    if all_sheet_updates:
                        self.logger.info(
                            f"Plausi-Batch: Sende {len(all_sheet_updates)} Operationen für {update_batch_limit_config} Zeilen...")
                        self.sheet_handler.batch_update_cells(
                            all_sheet_updates)
                        all_sheet_updates = []  # Liste leeren nach dem Senden
                        time.sleep(0.5)  # Kurze Pause nach Batch-Update

            # Ende der for-Schleife über die Zeilen

            # Sende verbleibende Updates
            if all_sheet_updates:
                self.logger.info(
                    f"Plausi-Batch: Sende verbleibende {len(all_sheet_updates)} Operationen...")
                self.sheet_handler.batch_update_cells(all_sheet_updates)

            self.logger.info(
                f"Modus 'plausi_check_data' abgeschlossen. {processed_rows_count} Zeilen verarbeitet.")

    def _suggest_parent_account_openai_task(self, task_data, openai_semaphore):
        """
        Fragt ChatGPT nach einem Parent Account für ein einzelnes Unternehmen.
        """
        """
        Fragt ChatGPT nach einem Parent Account für ein einzelnes Unternehmen.
        Läuft in einem separaten Thread für den Parent-Suggestion-Batch.

        Args:
            task_data (dict): Enthält die Daten für die Zeile
                              (row_num, crm_name, crm_website, crm_beschreibung,
                               wiki_url, wiki_absatz, wiki_kategorien, website_zusammenfassung).
            openai_semaphore (threading.Semaphore): Semaphore zur Begrenzung gleichzeitiger OpenAI-Calls.

        Returns:
            dict: {'row_num': int, 'suggested_parent': str, 'justification': str, 'error': str or None}
        """
        logger = logging.getLogger(__name__ + ".suggest_parent_task")
        row_num = task_data['row_num']
        suggested_parent = "k.A."
        justification = "Keine Begründung erhalten."
        error_msg = None

        # Bereinige Input-Daten für den Prompt
        crm_name = str(task_data.get('crm_name', 'N/A')).strip()
        crm_website = str(task_data.get('crm_website', 'N/A')).strip()
        crm_beschreibung = str(task_data.get(
            'crm_beschreibung', 'N/A')).strip()[:800]  # Gekürzt
        wiki_url = str(task_data.get('wiki_url', 'N/A')).strip()
        wiki_absatz = str(task_data.get('wiki_absatz', 'N/A')
                          ).strip()[:800]  # Gekürzt
        wiki_kategorien = str(task_data.get(
            'wiki_kategorien', 'N/A')).strip()[:500]  # Gekürzt
        website_zusammenfassung = str(task_data.get(
            'website_zusammenfassung', 'N/A')).strip()[:800]  # Gekürzt

        prompt_parts = [
            "Du bist ein Wirtschaftsanalyst und recherchierst Unternehmensstrukturen.",
            "Basierend auf den folgenden Informationen, identifiziere bitte den Namen der direkten Muttergesellschaft oder des übergeordneten Konzerns für das genannte Unternehmen.",
            "Wenn keine klare Muttergesellschaft ersichtlich ist oder das Unternehmen selbständig zu sein scheint, antworte mit 'k.A.' für den Parent Account.",
            "Gib deine Antwort ausschließlich im folgenden Format aus (keine Einleitung, kein Schlusssatz):",
            "Vorgeschlagener Parent Account: <Name des Parent Accounts oder k.A.>",
            "Begründung: <Sehr kurze Begründung für deinen Vorschlag oder warum keiner gemacht werden kann. Erwähne die Informationsquelle, falls möglich (z.B. Wikipedia Infobox, Website).>",
            "\n--- Unternehmensinformationen ---",
            f"Unternehmen: {crm_name}",
        ]
        if crm_website and crm_website.lower() != "n/a":
            prompt_parts.append(f"Website: {crm_website}")
        if crm_beschreibung and crm_beschreibung.lower() != "n/a":
            prompt_parts.append(f"CRM Beschreibung: {crm_beschreibung}")
        if wiki_url and "wikipedia.org" in wiki_url.lower():
            prompt_parts.append(f"Wikipedia URL: {wiki_url}")
            if wiki_absatz and wiki_absatz.lower() != "n/a":
                prompt_parts.append(f"Wikipedia Absatz: {wiki_absatz}")
            if wiki_kategorien and wiki_kategorien.lower() != "n/a":
                prompt_parts.append(f"Wikipedia Kategorien: {wiki_kategorien}")
        if website_zusammenfassung and website_zusammenfassung.lower(
        ) != "n/a" and not website_zusammenfassung.startswith("k.A. (Fehler"):
            prompt_parts.append(
                f"Website Zusammenfassung: {website_zusammenfassung}")

        prompt_parts.append(
            "\nBitte gib NUR die Antwort im oben genannten Format.")
        prompt = "\n".join(prompt_parts)

        # Token Count (optional, zur Info)
        # try:
        #     pt_count = token_count(prompt)
        #     logger.debug(f"Zeile {row_num}: Prompt für Parent Suggestion ({pt_count} Tokens): {prompt[:200]}...")
        # except Exception: pass

        try:
            with openai_semaphore:
                # call_openai_chat ist mit @retry_on_failure dekoriert
                raw_chat_response = call_openai_chat(
                    prompt, temperature=0.1, model=getattr(
                        Config, 'TOKEN_MODEL', 'gpt-4o-mini'))

            if raw_chat_response:
                parsed_parent = "k.A."
                parsed_justification = "Keine Begründung extrahiert."

                parent_match = re.search(
                    r"Vorgeschlagener Parent Account:\s*(.*)",
                    raw_chat_response,
                    re.IGNORECASE)
                if parent_match:
                    parsed_parent = parent_match.group(1).strip()
                    if not parsed_parent or parsed_parent.lower(
                    ) == "k.a.":  # Sicherstellen, dass "k.A." korrekt übernommen wird
                        parsed_parent = "k.A."

                justification_match = re.search(
                    r"Begründung:\s*(.*)", raw_chat_response, re.IGNORECASE)
                if justification_match:
                    parsed_justification = justification_match.group(1).strip()

                suggested_parent = parsed_parent
                justification = parsed_justification
                logger.debug(
                    f"Zeile {row_num}: ChatGPT Parent Vorschlag='{suggested_parent}', Begründung='{justification[:100]}...'")
            else:
                error_msg = "Leere Antwort von OpenAI erhalten."
                logger.warning(f"Zeile {row_num}: {error_msg}")
                justification = error_msg

        except Exception as e:
            error_msg = f"Fehler bei OpenAI Call für Parent Suggestion (Zeile {row_num}): {type(e).__name__} - {str(e)[:100]}"
            logger.error(error_msg)
            justification = error_msg

        return {
            "row_num": row_num,
            "suggested_parent": suggested_parent,
            "justification": justification,
            "error": error_msg}

    def process_parent_suggestion_batch(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None,
            re_evaluate_question_mark=False):
        """
        Batch-Prozess zur Generierung von Parent-Account-Vorschlägen.
        """
        self.logger.info(
            f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}")
        """
        Batch-Prozess zur Generierung von Parent-Account-Vorschlägen mittels ChatGPT.
        Schreibt Ergebnisse in Spalten O, P, Q.
        Bearbeitet nur Zeilen, bei denen Spalte Q (Parent Vorschlag Timestamp) leer ist,
        es sei denn re_evaluate_question_mark ist True und Spalte P ist '?'.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile.
            end_sheet_row (int, optional): Die 1-basierte Endzeile.
            limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen.
            re_evaluate_question_mark (bool, optional): Wenn True, werden auch Zeilen mit '?'
                                                       in Spalte P (Parent Vorschlag Status) erneut bewertet,
                                                       AUCH WENN Spalte Q bereits einen Timestamp hat.
        """
        self.logger.info(
            f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}, Re-Eval ?: {re_evaluate_question_mark}")

        # --- Daten laden und Startzeile ermitteln ---
        col_o_key = "System Vorschlag Parent Account"
        col_p_key = "Parent Vorschlag Status"
        col_q_key = "Parent Vorschlag Timestamp"  # Timestamp-Spalte für die Auswahl

        if start_sheet_row is None:
            # Geändert: Start basierend auf leerem Q
            self.logger.info(
                f"Automatische Ermittlung der Startzeile basierend auf leerem '{col_q_key}'...")
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key=col_q_key, min_sheet_row=7)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei autom. Startzeilenermittlung. Breche ab.")
                return
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1
            self.logger.info(
                f"Automatisch ermittelte Startzeile: {start_sheet_row}")
        else:
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "FEHLER beim Laden der Daten für Parent Suggestion Batch.")
                return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)

        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows
        self.logger.info(
            f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.")
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Start liegt nach Ende oder außerhalb des Sheets. Keine Verarbeitung.")
            return

        # --- Indizes ---
        required_keys = [
            col_o_key,
            col_p_key,
            col_q_key,
            "CRM Name",
            "CRM Website",
            "CRM Beschreibung",
            "Wiki URL",
            "Wiki Absatz",
            "Wiki Kategorien",
            "Website Zusammenfassung",
            "Version"]
        if not all(key in COLUMN_MAP for key in required_keys):
            missing = [k for k in required_keys if k not in COLUMN_MAP]
            self.logger.critical(
                f"FEHLER: Spaltenschlüssel für Parent Suggestion Batch fehlen: {missing}. Abbruch.")
            return

        col_o_letter = _get_col_letter(
            COLUMN_MAP[col_o_key] + 1)
        col_p_letter = _get_col_letter(
            COLUMN_MAP[col_p_key] + 1)
        col_q_letter = _get_col_letter(
            COLUMN_MAP[col_q_key] + 1)

        openai_sem = threading.Semaphore(
            getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3))
        max_workers = getattr(Config, 'MAX_BRANCH_WORKERS', 10)
        processing_batch_size = getattr(
            Config, 'PROCESSING_BRANCH_BATCH_SIZE', 10)
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        tasks_for_current_openai_batch = []
        all_sheet_updates = []
        processed_count = 0
        skipped_count = 0

        # Funktion zum Verarbeiten und Schreiben eines Batches (bleibt intern
        # gleich)
        def _execute_and_write_openai_batch(current_tasks):
            # ... (Code der inneren Funktion bleibt identisch wie im vorherigen Vorschlag) ...
            nonlocal processed_count
            if not current_tasks:
                return

            batch_start_log_row = current_tasks[0]['row_num']
            batch_end_log_row = current_tasks[-1]['row_num']
            self.logger.debug(
                f"\n--- Starte Parent Suggestion OpenAI Batch ({len(current_tasks)} Tasks, Zeilen {batch_start_log_row}-{batch_end_log_row}) ---")

            batch_results_list = []
            with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
                future_to_task_map = {
                    executor.submit(
                        self._suggest_parent_account_openai_task,
                        task,
                        openai_sem): task for task in current_tasks}
                for future in concurrent.futures.as_completed(
                        future_to_task_map):
                    task_info_orig = future_to_task_map[future]
                    try:
                        result_data = future.result()
                        batch_results_list.append(result_data)
                    except Exception as e_future:
                        self.logger.error(
                            f"Exception im Future für Parent Suggestion Zeile {task_info_orig['row_num']}: {e_future}")
                        batch_results_list.append({
                            "row_num": task_info_orig['row_num'],
                            "suggested_parent": "FEHLER_TASK",
                            "justification": str(e_future)[:150],
                            "error": str(e_future)
                        })

            self.logger.debug(
                f"  OpenAI Batch ({batch_start_log_row}-{batch_end_log_row}) abgeschlossen. {len(batch_results_list)} Ergebnisse erhalten.")

            if batch_results_list:
                now_ts_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                updates_for_this_batch = []

                for res_item in batch_results_list:
                    rn = res_item['row_num']
                    parent_val = res_item.get(
                        'suggested_parent', 'k.A. (Fehler)')

                    updates_for_this_batch.append(
                        {'range': f'{col_o_letter}{rn}', 'values': [[parent_val]]})
                    status_val = "?" if parent_val and parent_val.lower(
                    ) != "k.a." and not parent_val.startswith("FEHLER") else ""
                    updates_for_this_batch.append(
                        {'range': f'{col_p_letter}{rn}', 'values': [[status_val]]})
                    updates_for_this_batch.append(
                        {'range': f'{col_q_letter}{rn}', 'values': [[now_ts_str]]})

                    if res_item.get('justification'):
                        self.logger.debug(
                            f"Zeile {rn} - Parent Begründung: {res_item.get('justification')[:200]}...")

                all_sheet_updates.extend(updates_for_this_batch)

            # Zähle hier, wenn Tasks tatsächlich an OpenAI gingen
            processed_count += len(current_tasks)

            if len(all_sheet_updates) >= update_batch_row_limit * 3:
                self.logger.info(
                    f"Sende Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...")
                self.sheet_handler.batch_update_cells(all_sheet_updates)
                all_sheet_updates.clear()

            time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.5)
        # Ende der Hilfsfunktion _execute_and_write_openai_batch

        # Hauptschleife über die Zeilen
        for i in range(start_sheet_row, end_sheet_row + 1):
            # Limit-Prüfung erfolgt jetzt innerhalb der _execute_and_write_openai_batch
            # oder besser hier vor dem Sammeln von Tasks, um nicht unnötig zu
            # iterieren.
            if limit is not None and processed_count >= limit:
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) für Parent Suggestions erreicht.")
                break

            row_idx_list = i - 1
            if row_idx_list >= total_sheet_rows:
                break
            row = all_data[row_idx_list]

            if not any(cell and str(cell).strip() for cell in row):
                skipped_count += 1
                continue

            # Kriterien für Verarbeitung
            val_q_timestamp = self._get_cell_value_safe(
                row, col_q_key).strip()  # Timestamp aus Spalte Q
            val_p_status = self._get_cell_value_safe(
                row, col_p_key).strip()   # Status aus Spalte P

            needs_processing = False
            if not val_q_timestamp:  # Spalte Q (Timestamp) ist leer
                needs_processing = True
            # Neubewertung für Status "?" auch wenn Timestamp Q gesetzt ist
            elif re_evaluate_question_mark and val_p_status == "?":
                needs_processing = True
                self.logger.debug(
                    f"Zeile {i}: Wird trotz vorhandenem Timestamp in Q ('{val_q_timestamp}') verarbeitet, da P='?' und re_evaluate_question_mark=True.")

            if not needs_processing:
                skipped_count += 1
                continue

            # Daten für Task sammeln
            task_data = {
                "row_num": i,
                "crm_name": self._get_cell_value_safe(row, "CRM Name"),
                "crm_website": self._get_cell_value_safe(row, "CRM Website"),
                "crm_beschreibung": self._get_cell_value_safe(row, "CRM Beschreibung"),
                "wiki_url": self._get_cell_value_safe(row, "Wiki URL"),
                "wiki_absatz": self._get_cell_value_safe(row, "Wiki Absatz"),
                "wiki_kategorien": self._get_cell_value_safe(row, "Wiki Kategorien"),
                "website_zusammenfassung": self._get_cell_value_safe(row, "Website Zusammenfassung")
            }
            tasks_for_current_openai_batch.append(task_data)

            if len(tasks_for_current_openai_batch) >= processing_batch_size:
                _execute_and_write_openai_batch(tasks_for_current_openai_batch)
                tasks_for_current_openai_batch.clear()

        if tasks_for_current_openai_batch:
            _execute_and_write_openai_batch(tasks_for_current_openai_batch)

        if all_sheet_updates:
            self.logger.info(
                f"Sende finale Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...")
            self.sheet_handler.batch_update_cells(all_sheet_updates)

        self.logger.info(
            f"Parent Account Suggestion Batch abgeschlossen. {processed_count} Zeilen verarbeitet, {skipped_count} Zeilen übersprungen.")

    def _predict_technician_bucket(self, row_data):
        """
        Führt eine Vorhersage des Servicetechniker-Buckets für eine einzelne Zeile durch.
        Die Feature-Erstellung ist exakt auf den Trainingsprozess abgestimmt.
        """
        company_name = self._get_cell_value_safe(row_data, 'CRM Name').strip()
        self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)")

        if not self.is_setup_complete or self.model is None or self.imputer is None or self._expected_features is None:
            self.logger.error("ML-Artefakte (Modell/Imputer/Features) nicht initialisiert. Überspringe Vorhersage.")
            return "FEHLER Schaetzung (Setup fehlt)"
        
        try:
            # === Feature Erstellung (exakt wie im Training) ===

            # 1. Numerische Werte holen
            umsatz_val = get_numeric_filter_value(self._get_cell_value_safe(row_data, "Finaler Umsatz (Wiki>CRM)"), is_umsatz=True)
            ma_val = get_numeric_filter_value(self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)"), is_umsatz=False)
            
            umsatz_val = np.nan if umsatz_val == 0 else umsatz_val
            ma_val = np.nan if ma_val == 0 else ma_val

            # 2. 'is_part_of_group' Feature
            parent_d = self._get_cell_value_safe(row_data, "Parent Account Name").strip().lower()
            parent_o = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip().lower()
            parent_p = self._get_cell_value_safe(row_data, "Parent Vorschlag Status").strip().lower()
            is_group = 1 if (parent_d and parent_d != 'k.a.') or (parent_o and parent_o != 'k.a.' and parent_p == 'x') else 0

            # 3. Ratio & Log Features
            log_umsatz = np.log1p(umsatz_val) if pd.notna(umsatz_val) else np.nan
            log_ma = np.log1p(ma_val) if pd.notna(ma_val) else np.nan
            umsatz_pro_ma = (umsatz_val / ma_val) if pd.notna(umsatz_val) and pd.notna(ma_val) and ma_val > 0 else np.nan

            # 4. Branchen-Gruppen-Feature (entscheidende Korrektur)
            branche_ki_val = self._get_cell_value_safe(row_data, "Chat Vorschlag Branche")
            branch_group_map = {branch_name: details.get('gruppe', 'Sonstige') for branch_name, details in Config.BRANCH_GROUP_MAPPING.items()}
            branchen_gruppe = branch_group_map.get(branche_ki_val, 'Sonstige')

            # 5. DataFrame mit allen möglichen Features erstellen
            data_for_prediction = {
                'Log_Finaler_Umsatz_ML': log_umsatz,
                'Log_Finaler_Mitarbeiter_ML': log_ma,
                'Umsatz_pro_MA_ML': umsatz_pro_ma,
                'is_part_of_group': is_group,
            }
            
            # Füge die One-Hot-Encoded Branchen-Gruppen hinzu
            for expected_feature in self._expected_features:
                if expected_feature.startswith('Gruppe_'):
                    gruppe_name_from_column = expected_feature.replace('Gruppe_', '')
                    data_for_prediction[expected_feature] = 1 if gruppe_name_from_column == branchen_gruppe else 0
            
            df_processed = pd.DataFrame([data_for_prediction], columns=self._expected_features)

            # 6. Vorhersage durchführen
            # self.model ist die komplette Pipeline, die die Imputation intern durchführt
            prediction_proba = self.model.predict_proba(df_processed)
            predicted_bucket_label = self.model.classes_[np.argmax(prediction_proba[0])]

            self.logger.debug(f"  -> ML Vorhersage Ergebnis: '{predicted_bucket_label}'")
            return predicted_bucket_label

        except Exception as e_predict:
            self.logger.exception(f"FEHLER bei der ML-Vorhersage für Zeile ({company_name[:50]}...): {e_predict}")
            return f"FEHLER Schaetzung: {str(e_predict)[:100]}..."

    def _load_ml_model(self, model_path, imputer_path):
        """
        Laedt das trainierte ML-Modell, den Imputer und die Feature-Liste.
        """
        self.model, self.imputer, self._expected_features = None, None, None
        try:
            if not os.path.exists(
                    model_path) or not os.path.exists(imputer_path):
                self.logger.error("Modell- oder Imputer-Datei nicht gefunden.")
                return
            with open(model_path, 'rb') as f:
                self.model = pickle.load(f)
            with open(imputer_path, 'rb') as f:
                self.imputer = pickle.load(f)
            if os.path.exists(PATTERNS_FILE_JSON):
                with open(PATTERNS_FILE_JSON, 'r', encoding='utf-8') as f:
                    self._expected_features = json.load(
                        f).get("feature_columns")
            if self._expected_features:
                self.logger.info("ML-Modell, Imputer und Features geladen.")
            else:
                self.logger.error("Konnte erwartete Features nicht laden.")
        except Exception as e:
            self.logger.exception(f"FEHLER beim Laden von ML-Artefakten: {e}")

    def prepare_data_for_modeling(self):
        """
        Laedt und bereitet Daten für das ML-Training vor.
        """
        """
        Laedt Daten aus dem Google Sheet ueber den sheet_handler,
        bereitet sie fuer das Decision Tree Modell vor:
        - Waehlt relevante Spalten aus und benennt sie um.
        - Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Prioritaet).
        - Filtert nach gueltiger Technikerzahl (> 0).
        - Erstellt die Zielvariable (Techniker-Bucket).
        - Bereitet Features auf (One-Hot Encoding fuer Branche).
        - Behaelt NaNs in numerischen Features fuer spaetere Imputation.

        Returns:
            pandas.DataFrame: Vorbereiteter DataFrame fuer Training/Test-Split,
                              oder None bei Fehlern oder wenn keine gueltigen Trainingsdaten gefunden wurden.
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # <<< GEÄNDERT
        self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...")
        
        # Prüfen, ob der Sheet Handler initialisiert ist und Daten hat.
        # Wenn nicht, versuchen, die Daten zu laden.
        if not self.sheet_handler or not self.sheet_handler.get_all_data_with_headers():
            if not self.sheet_handler.load_data():
                self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.")
                return None
                self.logger.critical(
                    "Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.")  # <<< GEÄNDERT
                return None  # Gebe None zurueck, wenn Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        # Pruefe auf ausreichende Zeilenzahl (Header + mindestens eine
        # Datenzeile)
        min_required_rows = header_rows + 1
        # Wenn nicht genuegend Zeilen da sind
        if not all_data or len(all_data) < min_required_rows:
            self.logger.error(
                f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).")  # <<< GEÄNDERT
            return None  # Gebe None zurueck, wenn nicht genuegend Daten da sind

        # --- Header pruefen und DataFrame erstellen ---
        try:
            # Die erste Zeile sollte die Spaltennamen enthalten.
            headers = all_data[0]
            # Stellen Sie sicher, dass die Header-Zeile auch die erwartete Mindestlaenge hat,
            # um die Spaltenindizes aus COLUMN_MAP (Block 1) zu finden.
            try:
                # Finde den hoechsten Index in COLUMN_MAP
                max_col_idx_in_map = max(d['index'] for d in COLUMN_MAP.values() if 'index' in d)
                # Pruefen Sie, ob die Anzahl der geladenen Spalten im Header
                # ausreicht
                if len(headers) <= max_col_idx_in_map:
                    # Logge einen kritischen Fehler, wenn das Mapping auf
                    # Spalten zeigt, die nicht im Sheet existieren
                    self.logger.critical(
                        f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.")  # <<< GEÄNDERT
                    return None  # Beende die Methode
            except ValueError:  # Tritt auf, wenn COLUMN_MAP leer ist
                self.logger.critical(
                    "FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.")  # <<< GEÄNDERT
                return None  # Beende die Methode
            except Exception as e:
                # Fange andere unerwartete Fehler ab
                self.logger.critical(
                    f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}")  # <<< GEÄNDERT
                return None  # Beende die Methode

        except IndexError:
            # Wenn das Sheet leer ist oder keine erste Zeile hat
            self.logger.critical(
                "FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.")  # <<< GEÄNDERT
            return None  # Beende die Methode
        except Exception as e:
            # Fange andere unerwartete Fehler beim Zugriff auf Header ab
            self.logger.critical(
                f"FEHLER beim Zugriff auf Header: {e}")  # <<< GEÄNDERT
            # Logge den Traceback
            self.logger.debug(traceback.format_exc())  # <<< GEÄNDERT
            return None  # Beende die Methode

        # Datenzeilen sind alle Zeilen nach den Header-Zeilen
        # Annahme: Die ersten X Zeilen sind Header
        data_rows = all_data[header_rows:]

        # Erstelle DataFrame aus den Datenzeilen und den Headern
        df = pd.DataFrame(data_rows, columns=headers)
        self.logger.info(
            f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")  # <<< GEÄNDERT

        # DACH-Filter (basierend auf CRM Land - Spalte G)
        # Holt den tatsächlichen Spaltennamen
        crm_land_col_header = headers[get_col_idx("CRM Land")]
        # Erlaubte Werte für DACH-Länder (Groß- und Kleinschreibung wird durch
        # .str.upper() behandelt)
        dach_countries = [
            "DE",
            "CH",
            "AT",
            "DEUTSCHLAND",
            "ÖSTERREICH",
            "SCHWEIZ",
            "OESTERREICH"]  # OESTERREICH hinzugefügt

        # Sicherstellen, dass die Spalte existiert, bevor gefiltert wird
        if crm_land_col_header in df.columns:
            df = df[df[crm_land_col_header].astype(str).str.upper().isin(
                dach_countries)].copy()  # .copy() um Warnung zu vermeiden
            self.logger.info(
                f"Nach DACH-Filter (basierend auf '{crm_land_col_header}'): {len(df)} Zeilen verbleiben.")
            if df.empty:
                self.logger.error(
                    "Keine DACH-Unternehmen im Datensatz nach Filterung.")
                return None
        else:
            self.logger.error(
                f"Spalte '{crm_land_col_header}' für DACH-Filter nicht im DataFrame gefunden.")
            return None

        # Plausibilitätsfilter (basierend auf Spalten BG und BH)
        plausi_umsatz_col_header = headers[get_col_idx("Plausibilität Umsatz")]
        plausi_ma_col_header = headers[get_col_idx("Plausibilität Mitarbeiter")]

        # Sicherstellen, dass die Spalten existieren
        if plausi_umsatz_col_header in df.columns and plausi_ma_col_header in df.columns:
            # Filtere Zeilen, bei denen Plausi-Umsatz oder Plausi-MA einen Fehler anzeigt
            # Hier gehen wir davon aus, dass Fehler mit "FEHLER_" beginnen.
            df = df[~df[plausi_umsatz_col_header].astype(
                str).str.upper().str.startswith('FEHLER')].copy()
            df = df[~df[plausi_ma_col_header].astype(
                str).str.upper().str.startswith('FEHLER')].copy()
            self.logger.info(
                f"Nach Entfernung von FEHLER-Plausi-Fällen (BG, BH): {len(df)} Zeilen verbleiben.")
            if df.empty:
                self.logger.error("Keine Zeilen nach Plausi-Filterung übrig.")
                return None
            # Hier könnten Sie noch spezifischere Filter für bestimmte
            # WARNUNG-Typen einbauen, falls gewünscht
        else:
            self.logger.error(
                f"Plausibilitätsspalten '{plausi_umsatz_col_header}' oder '{plausi_ma_col_header}' nicht im DataFrame gefunden.")
            return None

        # --- Spaltenauswahl und Umbenennung ---
        # Definiere die notwendigen Spalten anhand ihrer COLUMN_MAP Schluessel (Block 1)
        # und weisen ihnen interne, einfachere Namen zu, die im DataFrame
        # verwendet werden.
        col_keys_mapping = {
            "name": "CRM Name",                     # Zur Identifikation, wird spaeter entfernt
            "branche_ki": "Chat Vorschlag Branche", # Fuer One-Hot Encoding
            "umsatz_crm": "CRM Umsatz",             # Fuer Konsolidierung
            "umsatz_wiki": "Wiki Umsatz",           # Fuer Konsolidierung
            "ma_crm": "CRM Anzahl Mitarbeiter",     # Fuer Konsolidierung
            "ma_wiki": "Wiki Mitarbeiter",          # Fuer Konsolidierung
            # DIE ZIELVARIABLE (Bekannte Technikerzahl)
            "techniker": "CRM Anzahl Techniker",
            # Spalte D <- Dies ist Zeile 9012
            "parent_d_raw": "Parent Account Name",
            "parent_o_raw": "System Vorschlag Parent Account",    # Spalte O
            "parent_p_raw": "Parent Vorschlag Status"             # Spalte P
        }

        # Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP
        # (Block 1) vorhanden sind
        missing_keys_in_map = [
            key for key in col_keys_mapping.values() if key not in COLUMN_MAP]
        if missing_keys_in_map:
            self.logger.critical(
                f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.")  # <<< GEÄNDERT
            return None  # Beende die Methode

        # Erstelle das Mapping von tatsaechlichen Header-Namen zu internen Schluesseln.
        # Verwende die Header-Namen aus dem geladenen Sheet und die COLUMN_MAP,
        # um die richtigen Header zu finden.
        header_to_internal_key = {}  # Dict zum Umbenennen der Spalten
        # Liste der Header-Namen, die aus dem DF ausgewaehlt werden
        cols_to_select_by_header = []

        try:
            # Iteriere ueber das Mapping von internen zu COLUMN_MAP Schluesseln
            for internal_key, column_map_key in col_keys_mapping.items():
                # Hole den tatsaechlichen Header-Namen aus dem Sheet
                header_name_from_sheet = headers[COLUMN_MAP[column_map_key]['index']]
                # Fuege das Mapping hinzu
                header_to_internal_key[header_name_from_sheet] = internal_key
                # Fuege den Header-Namen zur Liste der auszuwaehlenden Spalten
                # hinzu
                cols_to_select_by_header.append(header_name_from_sheet)

            # Waehle nur die benoetigten Spalten im DataFrame aus
            # Kopie erstellen, um SettingWithCopyWarning zu vermeiden
            df_subset = df[cols_to_select_by_header].copy()
            # Benenne die Spalten um zu den internen Namen
            df_subset.rename(columns=header_to_internal_key, inplace=True)

        except KeyError as e:
            # Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden,
            # tritt aber auf, wenn ein erwarteter Header-Name nicht im
            # geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist).
            self.logger.critical(
                f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.")  # <<< GEÄNDERT
            self.logger.debug(
                f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}")  # <<< GEÄNDERT
            return None  # Beende die Methode
        except IndexError as e:
            # Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat
            self.logger.critical(
                f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.")  # <<< GEÄNDERT
            self.logger.debug(
                f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.")  # <<< GEÄNDERT
            return None  # Beende die Methode
        except Exception as e:
            # Fange andere unerwartete Fehler ab
            self.logger.critical(
                f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}")  # <<< GEÄNDERT
            # Logge den Traceback
            self.logger.debug(traceback.format_exc())  # <<< GEÄNDERT
            return None  # Beende die Methode

        self.logger.info(
            f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}")  # <<< GEÄNDERT

        self.logger.info("Erstelle Feature 'is_part_of_group'...")

        # Zugreifen auf die Spalten im DataFrame df_subset
        # Die Spaltennamen hier müssen den internen Namen entsprechen,
        # die in col_keys_mapping definiert wurden (z.B. 'parent_d_raw').
        parent_d_series = df_subset['parent_d_raw'].astype(
            str).str.strip().str.lower()
        parent_o_series = df_subset['parent_o_raw'].astype(
            str).str.strip().str.lower()
        parent_p_series = df_subset['parent_p_raw'].astype(
            str).str.strip().str.lower()

        cond1 = parent_d_series.notna() & (
            parent_d_series != 'k.a.') & (
            parent_d_series != '')
        cond2_o = parent_o_series.notna() & (
            parent_o_series != 'k.a.') & (
            parent_o_series != '')
        cond2_p = parent_p_series == 'x'
        cond2 = cond2_o & cond2_p

        # .loc verwenden, um die neue Spalte sicher zuzuweisen und SettingWithCopyWarning zu vermeiden
        df_subset.loc[:, 'is_part_of_group'] = np.where(cond1 | cond2, 1, 0)

        self.logger.info(
            f"Feature 'is_part_of_group' erstellt. {df_subset['is_part_of_group'].sum()} Unternehmen als Teil einer Gruppe markiert.")
        self.logger.debug(
            f"Verteilung von 'is_part_of_group':\n{df_subset['is_part_of_group'].value_counts(normalize=True, dropna=False)}")

        # --- Features konsolidieren (Umsatz, Mitarbeiter) ---
        self.logger.debug(
            "Konsolidiere Umsatz und Mitarbeiter für ML-Features...")
        cols_to_process_ml = {
            'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz_ML'),
            'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter_ML')
        }
        for base_name, (wiki_col_ml, crm_col_ml,
                        final_col_ml) in cols_to_process_ml.items():
            is_umsatz_flag = (base_name == 'Umsatz')
            wiki_series = df_subset[wiki_col_ml].apply(
                lambda x: get_numeric_filter_value(
                    x, is_umsatz=is_umsatz_flag))
            crm_series = df_subset[crm_col_ml].apply(
                lambda x: get_numeric_filter_value(
                    x, is_umsatz=is_umsatz_flag))

            # Wähle Wiki-Wert, wenn vorhanden und > 0, sonst CRM-Wert
            df_subset.loc[:, final_col_ml] = np.where(
                (wiki_series.notna()) & (wiki_series > 0),
                wiki_series,
                crm_series
            )
            # Ersetze 0 explizit durch NaN, damit es von log1p und Imputer
            # korrekt behandelt wird
            df_subset.loc[:, final_col_ml] = df_subset[final_col_ml].replace(
                0, np.nan)
            self.logger.info(
                f"  -> {df_subset[final_col_ml].notna().sum()} gueltige '{final_col_ml}' Werte erstellt (von {len(df_subset)} Zeilen).")

        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ NEUER BLOCK: Feature Engineering (Ratio & Log-Transformationen) +
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        self.logger.info(
            "Erstelle zusätzliche Features (Ratio, Log-Transformationen)...")

        if 'Finaler_Umsatz_ML' in df_subset.columns and 'Finaler_Mitarbeiter_ML' in df_subset.columns:
            # Umsatz pro Mitarbeiter
            # Hier sind Nullen schon durch NaN ersetzt
            ma_for_ratio = df_subset['Finaler_Mitarbeiter_ML']
            df_subset.loc[:,
                          'Umsatz_pro_MA_ML'] = df_subset['Finaler_Umsatz_ML'] / ma_for_ratio
            df_subset['Umsatz_pro_MA_ML'].replace(
                [np.inf, -np.inf], np.nan, inplace=True)
            self.logger.debug(f"  -> Feature 'Umsatz_pro_MA_ML' erstellt.")

            # Log-Transformationen (np.log1p(x) berechnet log(1+x), sicher für
            # NaNs)
            df_subset.loc[:, 'Log_Finaler_Umsatz_ML'] = np.log1p(
                df_subset['Finaler_Umsatz_ML'])
            df_subset.loc[:, 'Log_Finaler_Mitarbeiter_ML'] = np.log1p(
                df_subset['Finaler_Mitarbeiter_ML'])
            self.logger.debug(f"  -> Log-transformierte Features erstellt.")
        else:
            self.logger.warning(
                "Konsolidierte Umsatz/Mitarbeiter-Spalten nicht gefunden, Feature Engineering übersprungen.")
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ ENDE NEUER BLOCK ++++++++++++++++++++++++++++++++++++++++++++++++
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # --- Zielvariable vorbereiten (Technikerzahl) ---
        self.logger.info("Verarbeite Zielvariable 'techniker'...")
        techniker_col_internal = "techniker"
        df_subset.loc[:, 'Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(
            lambda x: get_numeric_filter_value(x, is_umsatz=False))

        initial_rows_before_tech_filter = len(df_subset)
        df_filtered = df_subset[
            df_subset['Anzahl_Servicetechniker_Numeric'].notna() &
            (df_subset['Anzahl_Servicetechniker_Numeric'] > 0)
        ].copy()

        removed_rows_tech_filter = initial_rows_before_tech_filter - \
            len(df_filtered)
        if removed_rows_tech_filter > 0:
            self.logger.info(
                f"{removed_rows_tech_filter} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl.")
        self.logger.info(
            f"Verbleibende Zeilen fuer Modellierungstraining: {len(df_filtered)}")

        if df_filtered.empty:
            self.logger.error(
                "FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!")
            return None

        # --- Techniker-Buckets erstellen (mit reduzierter Klassenanzahl) ---
        self.logger.info(
            "Erstelle reduzierte Techniker-Buckets (3 Klassen)...")
        bins_new = [-1, 49, 249, float('inf')]
        labels_new = [
            'Techniker_Klein (0-49)',
            'Techniker_Mittel (50-249)',
            'Techniker_Gross (250+)']

        df_filtered.loc[:, 'Techniker_Bucket'] = pd.cut(df_filtered['Anzahl_Servicetechniker_Numeric'],
                                                     bins=bins_new,
                                                     labels=labels_new,
                                                     right=True,
                                                     include_lowest=True)
        self.logger.info(
            f"Verteilung der neuen Techniker-Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True, dropna=False).sort_index().round(3)}")

        # --- Kategoriale Features vorbereiten (Branchen-Gruppen) ---
        # Wir verwenden konsistent die KI-generierte Spalte 'branche_ki' als Basis für die Gruppierung.
        branche_col_internal = "branche_ki"
        self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' und mappe es zu 'Branchen_Gruppe'...")

        if branche_col_internal not in df_filtered.columns:
            self.logger.critical(f"FEHLER: Die für das Mapping benötigte Spalte '{branche_col_internal}' wurde nicht im DataFrame gefunden. Breche ab.")
            return None

        # Wende das saubere Mapping aus der Config an, um nur den 'gruppe'-String zu extrahieren.
        branch_group_map = {branch_name: details.get('gruppe', 'Sonstige') for branch_name, details in Config.BRANCH_GROUP_MAPPING.items()}
        df_filtered.loc[:, 'Branchen_Gruppe'] = df_filtered[branche_col_internal].map(branch_group_map).fillna('Sonstige')

        self.logger.info("Mapping zu 'Branchen_Gruppe' durchgeführt.")
        self.logger.debug(
            f"Verteilung der Branchen-Gruppen:\n{df_filtered['Branchen_Gruppe'].value_counts(normalize=True).sort_index().round(3)}")

        # --- One-Hot Encoding ---
        df_encoded = pd.get_dummies(df_filtered, columns=['Branchen_Gruppe'], prefix='Gruppe', dummy_na=False)
        self.logger.info("One-Hot Encoding fuer 'Branchen_Gruppe' durchgefuehrt.")

        # --- Finale Auswahl der Features fuer das Modell ---
        feature_columns_ml = [col for col in df_encoded.columns if col.startswith('Gruppe_')]
        feature_columns_ml.extend(['Log_Finaler_Umsatz_ML', 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML', 'is_part_of_group'])
        self.logger.info(f"Finale Feature-Auswahl für das Training: {feature_columns_ml}")

        target_column_ml = 'Techniker_Bucket'
        identification_cols_ml = ['name', 'Anzahl_Servicetechniker_Numeric']

        final_cols_for_df_ml = identification_cols_ml + feature_columns_ml + [target_column_ml]
        missing_final_cols_ml = [col for col in final_cols_for_df_ml if col not in df_encoded.columns]
        if missing_final_cols_ml:
            self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols_ml}")
            return None

        df_model_ready = df_encoded[final_cols_for_df_ml].copy()

        numeric_features_to_convert = ['Log_Finaler_Umsatz_ML', 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML', 'Anzahl_Servicetechniker_Numeric']
        for col in numeric_features_to_convert:
            if col in df_model_ready.columns:
                df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce')

        df_model_ready = df_model_ready.reset_index(drop=True)

        self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.")
        self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.")
        self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns_ml)}")

        numeric_features_for_imputation_ml = ['Log_Finaler_Umsatz_ML', 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML']
        existing_numeric_features = [col for col in numeric_features_for_imputation_ml if col in df_model_ready.columns]
        if existing_numeric_features:
            nan_counts = df_model_ready[existing_numeric_features].isna().sum()
            self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")
            rows_with_nan = df_model_ready[existing_numeric_features].isna().any(axis=1).sum()
            self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}")

        return df_model_ready

    def train_technician_model(
            self,
            model_out=MODEL_FILE,
            imputer_out=IMPUTER_FILE,
            patterns_out=PATTERNS_FILE_JSON):
        """
        Trainiert, evaluiert und speichert das ML-Modell für die Techniker-Schätzung.
        """
        self.logger.info("Starte Training des Servicetechniker-Modells...")
        self.logger.info(
            "Starte Training des Servicetechniker Decision Tree Modells...")

        # 1. Daten vorbereiten
        df_model_ready = self.prepare_data_for_modeling()
        if df_model_ready is None or df_model_ready.empty:
            self.logger.error(
                "Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.")
            return

        # Feature Spalten und Zielspalte definieren
        identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric']
        target_column = 'Techniker_Bucket'
        feature_columns_ml = [
            col for col in df_model_ready.columns if col not in identification_cols and col != target_column]

        if not feature_columns_ml:
            self.logger.critical(
                "FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.")
            return

        X = df_model_ready[feature_columns_ml]
        y = df_model_ready[target_column]
        self.logger.info(
            f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}")

        # 2. Split in Training und Test Set
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.25, random_state=42, stratify=y)
        self.logger.info(
            f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.")

        # 3. Pipeline-Definition (Imputation wird Teil der Pipeline)
        # Schritt 1: Imputer für fehlende Werte
        # Schritt 2: SMOTE für Klassen-Balancierung
        # Schritt 3: RandomForestClassifier als Modell
        pipeline = ImbPipeline([
            ('imputer', SimpleImputer(strategy='median')),
            ('smote', SMOTE(random_state=42)),
            ('classifier', RandomForestClassifier(random_state=42, n_jobs=-1))
        ])

        # 4. Hyperparameter-Grid für GridSearchCV definieren
        # Die Parameternamen müssen mit dem Namen des Schritts in der Pipeline beginnen.
        param_grid = {
            'classifier__n_estimators': [200, 300],
            'classifier__max_depth': [10, 20, None],
            'classifier__min_samples_split': [2, 5],
            'classifier__min_samples_leaf': [1, 2]
        }

        # 5. GridSearchCV initialisieren
        grid_search = GridSearchCV(
            estimator=pipeline,
            param_grid=param_grid,
            cv=3,
            scoring='accuracy',
            verbose=2,
            n_jobs=-1)

        self.logger.info("Starte Hyperparameter-Tuning mit GridSearchCV...")
        start_fit_time = time.time()
        
        # 6. Grid auf den Original-Trainingsdaten fitten (ohne vorherige Imputation)
        # Die Pipeline kümmert sich intern um die korrekte Imputation und SMOTE für jeden Fold.
        grid_search.fit(X_train, y_train)
        end_fit_time = time.time()
        self.logger.info(
            f"GridSearchCV-Suche abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.")

        # Beste Parameter und bestes Modell ausgeben und speichern
        self.logger.info(
            f"Beste gefundene Parameter: {grid_search.best_params_}")
        self.logger.info(
            f"Beste Cross-Validation Accuracy: {grid_search.best_score_:.4f}")

        # Das beste Modell ist das, das mit den besten Parametern auf den
        # *gesamten* Trainingsdaten trainiert wurde.
        best_classifier = grid_search.best_estimator_

        # Modell speichern
        self.model = best_classifier  # Zuweisung des besten gefundenen Modells
        try:
            model_dir = os.path.dirname(model_out)
            if model_dir and not os.path.exists(model_dir):
                os.makedirs(model_dir, exist_ok=True)
            with open(model_out, 'wb') as f:
                pickle.dump(best_classifier, f)  # Das beste Modell speichern
            self.logger.info(
                f"Bestes RandomForest Modell erfolgreich gespeichert in '{model_out}'.")
        except Exception as e:
            self.logger.error(
                f"FEHLER beim Speichern des besten Modells in '{model_out}': {e}")
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ ENDE ANPASSUNG ++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # Feature-Liste speichern (bleibt unverändert)
        self._expected_features = feature_columns_ml
        try:
            # Wir verwenden die .classes_ vom besten gefundenen Modell
            patterns_data = {
                "feature_columns": self._expected_features,
                "target_classes": list(
                    best_classifier.classes_)}  # << KORRIGIERT
            patterns_dir = os.path.dirname(patterns_out)
            if patterns_dir and not os.path.exists(patterns_dir):
                os.makedirs(patterns_dir, exist_ok=True)
            with open(patterns_out, 'w', encoding='utf-8') as f:
                json.dump(patterns_data, f, indent=4, ensure_ascii=False)
            self.logger.info(
                f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.")
        except Exception as e:
            self.logger.error(
                f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}")
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # +++ ENDE FEATURE-LISTEN SPEICHERUNG +++++++++++++++++++++++++++++++++
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu
        # bewerten)
        self.logger.info(
            "Starte Evaluation des besten Modells auf dem ungesehenen Testset...")
        y_pred = best_classifier.predict(X_test)
        accuracy = accuracy_score(y_test, y_pred)
        self.logger.info(f"Finale Modell Genauigkeit auf dem Testset: {accuracy:.4f}")

        # Erzeuge den Klassifikationsbericht als Dictionary für eine saubere Ausgabe
        report_dict = classification_report(
            y_test,
            y_pred,
            zero_division=0,
            labels=self.model.classes_,
            target_names=[str(c) for c in self.model.classes_],
            output_dict=True
        )
        
        # Konvertiere das Dictionary in einen formatierten DataFrame für das Logging
        report_df = pd.DataFrame(report_dict).transpose()
        report_df['support'] = report_df['support'].astype(int) # Für saubere Darstellung
        
        # Erstelle einen gut lesbaren String aus dem DataFrame
        report_string = report_df.to_string()
        self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{report_string}")

        cm = confusion_matrix(y_test, y_pred, labels=self.model.classes_)
        cm_df = pd.DataFrame(cm, index=self.model.classes_, columns=self.model.classes_)
        self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm_df.to_string()}")

        # Block für Feature Importance
        try:
            # Greife auf den Schritt 'classifier' in der Pipeline zu, um das
            # finale Modell zu bekommen
            final_rf_model = best_classifier.named_steps['classifier']
            self.logger.info("Feature Importance des besten Modells (Top 15):")
            importances = final_rf_model.feature_importances_  # << KORRIGIERT
            feature_importance_df = pd.DataFrame({
                'Feature': feature_columns_ml,
                'Importance': importances
            }).sort_values(by='Importance', ascending=False)

            self.logger.info(
                f"\n{feature_importance_df.head(15).to_string(index=False)}")
        except Exception as e_feat_imp:
            self.logger.warning(
                f"FEHLER beim Berechnen/Anzeigen der Feature Importance: {e_feat_imp}")

        self.logger.info("Modelltraining und -evaluation abgeschlossen.")

    def process_predict_technicians(self, start_sheet_row=None, end_sheet_row=None, limit=None):
        """
        Batch-Prozess zur Vorhersage von Techniker-Buckets für Zeilen,
        bei denen diese Information fehlt.
        """
        self.logger.info(f"Starte Modus 'predict_technicians' (Batch). Bereich: {start_sheet_row or 'Start'}-{end_sheet_row or 'Ende'}, Limit: {limit or 'Unbegrenzt'}")

        # Prüfen, ob das Modell geladen ist (passiert im setup())
        if not self.is_setup_complete or self.model is None or self.imputer is None or self._expected_features is None:
            self.logger.error("ML-Artefakte nicht initialisiert. Bitte zuerst trainieren. Breche Vorhersage ab.")
            return

        if not self.sheet_handler.load_data():
            self.logger.error("Fehler beim Laden der Daten für die Vorhersage.")
            return

        all_data = self.sheet_handler.get_all_data_with_headers()
        header_rows = self.sheet_handler._header_rows
        
        effective_start = start_sheet_row if start_sheet_row is not None else header_rows + 1
        effective_end = end_sheet_row if end_sheet_row is not None else len(all_data)

        tasks = []
        for i in range(effective_start - 1, effective_end):
            if limit is not None and len(tasks) >= limit:
                break
            
            row_data = all_data[i]
            bucket_value = self._get_cell_value_safe(row_data, "Geschaetzter Techniker Bucket").strip()
            # Kriterium: Bucket ist leer oder enthält einen Fehler
            if not bucket_value or "fehler" in bucket_value.lower():
                tasks.append({'row_num': i + 1, 'data': row_data})
        
        if not tasks:
            self.logger.info("Keine Zeilen für die Techniker-Vorhersage gefunden.")
            return

        self.logger.info(f"{len(tasks)} Zeilen für die Vorhersage identifiziert. Starte Verarbeitung...")
        
        all_updates = []
        processed_count = 0
        update_batch_size = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)
        
        for idx, task in enumerate(tasks):
            row_num = task['row_num']
            row_data = task['data']
            try:
                # Aufruf der internen Methode zur Vorhersage für die einzelne Zeile
                predicted_bucket = self._predict_technician_bucket(row_data)
                
                col_idx = get_col_idx("Geschaetzter Techniker Bucket")
                if col_idx is not None:
                    col_letter = _get_col_letter(col_idx + 1)
                    all_updates.append({'range': f'{col_letter}{row_num}', 'values': [[predicted_bucket]]})
                
                processed_count += 1
            except Exception as e:
                self.logger.error(f"Fehler bei der Vorhersage für Zeile {row_num}: {e}", exc_info=True)

            # Batch-Update-Logik
            if (idx + 1) % update_batch_size == 0 or (idx + 1) == len(tasks):
                if all_updates:
                    self.logger.info(f"Sende Batch-Update für {len(all_updates)} Vorhersagen...")
                    self.sheet_handler.batch_update_cells(all_updates)
                    all_updates = []
        
        self.logger.info(f"Techniker-Vorhersage abgeschlossen. {processed_count} Zeilen bearbeitet.")


    def process_website_details(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        EXPERIMENTELL: Extrahiert Website-Details für Zeilen mit 'x' in Spalte A.
        """
        """
        EXPERIMENTELL: Extrahiert Website-Details fuer Zeilen, die in Spalte A mit 'x' markiert sind.
        Schreibt die Details in eine definierte Spalte (Website Details oder AR als Fallback).
        Loescht NICHT das 'x'-Flag.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge den Start des Modus auf Warning, da es experimentell ist.
        self.logger.warning(
            f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT
        self.logger.warning(
            "Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.")  # <<< GEÄNDERT

        # --- Daten laden ---
        # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier,
        # da wir explizit nach dem 'x'-Flag suchen.
        # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
        if not self.sheet_handler.load_data():
            self.logger.error(
                "Fehler beim Laden der Daten fuer Website Details Extraction.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Standard Startzeile, wenn nicht manuell gesetzt
        if start_sheet_row is None:
            # Standardmaessig ab erster Datenzeile (Zeile nach Headern)
            start_sheet_row = header_rows + 1

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den Suchbereich fuer das 'x'-Flag
        self.logger.info(
            f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes und Buchstaben ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = ["ReEval Flag", "CRM Website", "CRM Name"]  # A, D, B
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Indizes
        reeval_col_idx = col_indices["ReEval Flag"]  # A
        website_col_idx = col_indices["CRM Website"]  # D

        # Bestimme die ZIELSPALTE fuer die Details (Website Details ODER AR als
        # Fallback)
        # Versuche zuerst die dedizierte Spalte (Block 1 Column Map)
        details_col_idx = COLUMN_MAP.get("Website Details")
        details_col_key_for_logging = "Website Details"  # Name fuer Logging
        # Wenn die dedizierte Spalte nicht gefunden wurde
        if details_col_idx is None:
            # Fallback auf 'Website Rohtext' (AR)
            details_col_idx = COLUMN_MAP.get(
                "Website Rohtext")  # Block 1 Column Map
            details_col_key_for_logging = "Website Rohtext"
            # Pruefen Sie, ob der Fallback-Schluessel gefunden wurde
            if details_col_idx is None:
                self.logger.critical(
                    "FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.")  # <<< GEÄNDERT
                return  # Beende die Methode bei kritischem Fehler
            self.logger.warning(
                f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({_get_col_letter(details_col_idx+1)}) als Fallback.")  # <<< GEÄNDERT
        else:
            # Logge die Verwendung der dedizierten Spalte
            self.logger.info(
                f"Nutze Spalte '{details_col_key_for_logging}' ({_get_col_letter(details_col_idx+1)}) fuer Website Details.")  # <<< GEÄNDERT

        # Ermitteln Sie den Spaltenbuchstaben der Zielspalte (nutzt interne
        # Helfer _get_col_letter Block 14)
        details_col_letter = _get_col_letter(
            details_col_idx + 1)

        # --- Verarbeitung ---
        # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1).
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
        all_sheet_updates = []

        # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den
        # Batch aufgenommen werden (im Rahmen des Limits).
        processed_count = 0
        # Zaehlt Zeilen, die uebersprungen wurden (nicht markiert oder fehlende
        # URL).
        skipped_count = 0

        # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).")
                # # Zu viel Laerm im Debug
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: Zeile ist mit 'x' in Spalte A (ReEval Flag) markiert.
            # UND Website URL (D) ist vorhanden und gueltig aussehend.

            # Holen Sie den Wert aus Spalte A (ReEval Flag) (nutzt interne
            # Helfer _get_cell_value_safe)
            cell_a_value = self._get_cell_value_safe(
                row, "ReEval Flag").strip().lower()  # Block 1 Column Map
            # Pruefen Sie, ob die Zelle mit 'x' markiert ist.
            is_marked_for_processing = cell_a_value == "x"

            # Wenn die Zeile nicht mit 'x' markiert ist, ueberspringen
            if not is_marked_for_processing:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # Holen Sie den Wert aus Spalte D (CRM Website) (nutzt interne
            # Helfer _get_cell_value_safe)
            website_url = self._get_cell_value_safe(
                row, "CRM Website").strip()  # Block 1 Column Map
            # Pruefen Sie, ob die Website URL (D) vorhanden und gueltig
            # aussehend ist.
            website_url_is_valid_looking = website_url and isinstance(
                website_url,
                str) and website_url.lower() not in [
                "k.a.",
                "kein artikel gefunden",
                "fehler bei suche",
                "http:"]  # Fuege "http:" hinzu basierend auf Log

            # Verarbeitung ist noetig, wenn die Zeile mit 'x' markiert ist UND
            # die Website URL gueltig ist.
            processing_needed_for_row = is_marked_for_processing and website_url_is_valid_looking

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                company_name = self._get_cell_value_safe(
                    row, "CRM Name").strip()  # Block 1 Column Map
                self.logger.debug(
                    f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist (trotz
            # 'x' fehlte die URL)
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                # Optionale Behandlung: Wenn mit 'x' markiert, aber URL fehlt, was tun?
                # Derzeit wird sie uebersprungen. Ggf. Fehler in Spalte
                # notieren?
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Fuehre Details-Extraktion aus ---
            # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im
            # Rahmen des Limits zaehlen)
            processed_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            # <<< GEÄNDERT (war selflogger)
            self.logger.info(
                f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...")

            # Default Fehler, falls die Funktion nicht existiert (Sollte nicht
            # passieren, wenn Block 13 korrekt ist)
            details = "FEHLER: Funktion 'scrape_website_details' nicht verfuegbar"

            try:
                # Rufe die globale Funktion scrape_website_details auf (Block 13).
                # scrape_website_details ist mit retry_on_failure dekoriert (Block 2).
                # Wenn scrape_website_details fehlschlaegt, wirft sie eine
                # Exception oder gibt einen Fehlerwert zurueck.
                # <<< Ruft globale Funktion (Block 13)
                details = scrape_website_details(website_url)

                # Wenn die Funktion einen Fehler geloggt hat und einen Fehlerstring im Ergebnis zurueckgibt,
                # wird dies in der 'details' Variable gespeichert.
                if isinstance(details, str) and (details.startswith(
                        "k.A. (Fehler") or details.startswith("FEHLER:")):
                    # Fehler wurde bereits in scrape_website_details geloggt.
                    pass  # Details enthaelt bereits den Fehlerstring.

                elif not isinstance(details, str) or not details.strip():
                    # Wenn die Funktion keinen String oder einen leeren String
                    # zurueckgibt.
                    # Standard-Fehlerwert
                    details = "k.A. (Extraktion leer oder ungueltig)"

            except NameError:
                # Dieser Fehler sollte nicht auftreten, wenn
                # scrape_website_details in Block 13 ist.
                self.logger.critical(
                    "FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.")  # <<< GEÄNDERT
                # Logge den Traceback.
                self.logger.debug(traceback.format_exc())  # <<< GEÄNDERT
                details = "FEHLER: Funktion nicht definiert"  # Setze spezifischen Fehlerwert

            except Exception as e_detail:
                # Fange andere unerwartete Fehler ab, die nicht von
                # scrape_website_details behandelt wurden.
                self.logger.exception(
                    f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}")  # <<< GEÄNDERT
                # Signalisiert Fehler (gekuerzt)
                details = f"k.A. (Unerwarteter Fehler: {str(e_detail)[:100]}...)"

            # Fuege Update fuer die Details-Spalte hinzu (nutzt interne Helfer _get_col_letter Block 14)
            # Stellen Sie sicher, dass der Wert ein String ist.
            updates_for_row = []  # Lokale Liste fuer Updates dieser Zeile
            updates_for_row.append({'range': f'{details_col_letter}{i}', 'values': [
                                   [str(details)]]})  # Block 1 Column Map
            self.logger.debug(
                f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.")  # <<< GEÄNDERT

            # Sammle die Updates fuer diese Zeile in der globalen Liste
            # all_sheet_updates.
            all_sheet_updates.extend(updates_for_row)

            # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist.
            # update_batch_row_limit wird aus Config geholt (Block 1).
            # Updates pro Zeile ist 1 in diesem Modus. Anzahl der Zeilen =
            # len(all_sheet_updates).
            if len(all_sheet_updates) >= update_batch_row_limit:
                self.logger.debug(
                    f"  Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
                # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
                # Wenn es fehlschlaegt, wird es intern geloggt.
                success = self.sheet_handler.batch_update_cells(
                    all_sheet_updates)
                if success:
                    self.logger.info(
                        f"  Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")  # <<< GEÄNDERT
                # Der Fehlerfall wird von batch_update_cells geloggt

                # Leere die gesammelten Updates nach dem Senden.
                all_sheet_updates = []

            # Kleine Pause nach jeder Extraktion (nutzt Config Block 1).
            # Dieser Modus macht API calls (ueber scrape_website_details und
            # dessen Helfer), also Pause einbauen.
            pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2
            # self.logger.debug(f"Warte {pause_duration:.2f}s nach
            # Extraktion...") # Zu viel Laerm im Debug
            time.sleep(pause_duration)

        # --- Finale Sheet Updates senden ---
        # Sende alle verbleibenden gesammelten Updates in einem letzten
        # Batch-Update.
        if all_sheet_updates:
            self.logger.info(
                f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
            # Nutzt die batch_update_cells Methode des Sheet Handlers (Block
            # 14) mit Retry.
            success = self.sheet_handler.batch_update_cells(all_sheet_updates)
            if success:
                # <<< GEÄNDERT
                self.logger.info(f"FINALES Sheet-Update erfolgreich.")
            # Der Fehlerfall wird von batch_update_cells geloggt

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")  # <<< GEÄNDERT

    def process_wiki_updates_from_chatgpt(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Verarbeitet Wiki-Updates basierend auf ChatGPT-Vorschlägen.
        """
        """
        Identifiziert Zeilen, in denen Status S gesetzt ist, aber NICHT auf einem Endzustand
        (OK, X (UPDATED/COPIED/INVALID)), prueft ob U eine *valide* und *andere* Wiki-URL ist.
        - Wenn ja: Kopiert U->M, markiert S='X (URL Copied)', U='URL uebernommen', loescht
          abhaengige Wiki-Spalten (N-V, AN, AO, AP, AX), setzt ReEval-Flag A='x'.
        - Wenn nein (U keine URL, U==M, oder U ungueltig): LOESCHT den Inhalt von U und
          markiert S als 'X (Invalid Suggestion)'.
        Verarbeitet maximal limit Zeilen.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU PRUEFENDER Zeilen. Defaults to None (Unbegrenzt).
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge die Konfiguration des Modus
        self.logger.info(
            f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT

        # --- Daten laden ---
        # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier,
        # da wir nach Status S suchen.
        # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
        if not self.sheet_handler.load_data():
            self.logger.error(
                "Fehler beim Laden der Daten fuer Wiki Updates.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Standard Startzeile, wenn nicht manuell gesetzt
        if start_sheet_row is None:
            # Standardmaessig ab erster Datenzeile (Zeile nach Headern)
            start_sheet_row = header_rows + 1

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den Suchbereich fuer Status S
        self.logger.info(
            f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes und Buchstaben ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = [
            # S, U, M (Pruefkriterien / Daten)
            "Chat Wiki Konsistenzpruefung", "Chat Vorschlag Wiki Artikel", "Wiki URL",
            # AN, AX, AO, AP (Spalten zum Loeschen)
            "Wikipedia Timestamp", "Wiki Verif. Timestamp", "Timestamp letzte Pruefung", "Version",
            "ReEval Flag",  # A (ReEval Flag setzen)
            # N-R (Spalten zum Loeschen)
            "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien",
            # T, V (Spalten zum Loeschen)
            "Chat Begründung Wiki Inkonsistenz", "Begruendung bei Abweichung",
            # AY (SerpAPI Wiki Search Timestamp) wird ebenfalls geleert, da
            # abhaengig von M.
            "SerpAPI Wiki Search Timestamp"  # AY (Spalte zum Leeren)
        ]
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Spaltenbuchstaben fuer Updates/Leerung (nutzt
        # interne Helfer _get_col_letter Block 14)
        s_letter = _get_col_letter(
            col_indices["Chat Wiki Konsistenzpruefung"] + 1)  # Status S
        u_letter = _get_col_letter(
            col_indices["Chat Vorschlag Wiki Artikel"] + 1)  # Vorschlag U
        m_letter = _get_col_letter(
            col_indices["Wiki URL"] + 1)  # Wiki URL M
        a_letter = _get_col_letter(
            col_indices["ReEval Flag"] + 1)  # ReEval Flag A

        # Spalten N-V leeren.
        # N ist Wiki Absatz, V ist Begruendung bei Abweichung.
        n_idx = col_indices["Wiki Absatz"]
        v_idx = col_indices["Begruendung bei Abweichung"]
        # Erstellen Sie den Bereichsnamen (z.B. "N:V")
        n_letter = _get_col_letter(n_idx + 1)
        v_letter = _get_col_letter(v_idx + 1)
        nv_range_letter = f'{n_letter}:{v_letter}'  # z.B. N:V
        # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
        # Anzahl der Spalten = V_Index - N_Index + 1
        empty_nv_values = [''] * (v_idx - n_idx + 1)

        # Timestamps AN, AO, AX, AP, AY leeren.
        an_letter = _get_col_letter(
            col_indices["Wikipedia Timestamp"] + 1)  # AN (Wiki Extraction TS)
        ao_letter = _get_col_letter(
            col_indices["Timestamp letzte Pruefung"] +
            1)  # AO (Chat Evaluation TS)
        ap_letter = _get_col_letter(
            col_indices["Version"] + 1)  # AP (Version)
        ax_letter = _get_col_letter(
            col_indices["Wiki Verif. Timestamp"] + 1)  # AX (Wiki Verif. TS)
        ay_letter = _get_col_letter(
            col_indices["SerpAPI Wiki Search Timestamp"] +
            1)  # AY (SerpAPI Wiki TS)

        # --- Verarbeitung ---
        # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1).
        update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)

        # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
        all_sheet_updates = []

        # Zaehlt Zeilen, die geprueft werden (im Rahmen des Limits zaehlen).
        processed_rows_count = 0
        # Zaehlt Zeilen, die uebersprungen werden (Status S im Endzustand
        # etc.).
        skipped_count = 0
        updated_url_count = 0    # Zaehlt Zeilen, wo U -> M kopiert wurde.
        # Zaehlt Zeilen, wo Vorschlag U geloescht wurde.
        cleared_suggestion_count = 0

        # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).")
                # # Zu viel Laerm im Debug
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: Status S ist gesetzt (nicht leer) UND NICHT einer der Endzustaende.
            # Endzustaende: "OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID
            # SUGGESTION)"

            # Holen Sie den Wert aus Spalte S (Chat Wiki Konsistenzpruefung)
            # (nutzt interne Helfer _get_cell_value_safe)
            s_value = self._get_cell_value_safe(
                row, "Chat Wiki Konsistenzpruefung").strip()  # Block 1 Column Map
            s_value_upper = s_value.upper()

            # Definieren Sie die Endzustaende (Grossbuchstaben)
            s_end_states = [
                "OK",
                "X (UPDATED)",
                "X (URL COPIED)",
                "X (INVALID SUGGESTION)"]

            # Verarbeitung ist noetig, wenn S nicht leer ist UND S NICHT im
            # Endzustand ist.
            processing_needed_for_row = s_value and s_value_upper not in s_end_states

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                self.logger.debug(
                    f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Pruefe Vorschlag U und handle ---
            # Zaehle die Zeile, die geprueft wird (im Rahmen des Limits
            # zaehlen).
            processed_rows_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_rows_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            # Holen Sie die Werte aus Spalte U (Chat Vorschlag Wiki Artikel)
            # und M (Wiki URL) (nutzt interne Helfer _get_cell_value_safe)
            vorschlag_u = self._get_cell_value_safe(
                row, "Chat Vorschlag Wiki Artikel").strip()  # Block 1 Column Map
            url_m = self._get_cell_value_safe(
                row, "Wiki URL").strip()  # Block 1 Column Map

            self.logger.info(
                f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...")  # <<< GEÄNDERT

            # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden
            # soll.
            is_update_candidate = False
            new_url = ""  # Die URL, die ggf. in M kopiert wird.

            # Kriterium 1: Ist Vorschlag U ueberhaupt ein String und sieht nach
            # Wikipedia aus?
            condition1_u_is_wiki_url = vorschlag_u and isinstance(
                vorschlag_u,
                str) and "wikipedia.org/wiki/" in vorschlag_u.lower() and vorschlag_u.lower().startswith(
                ("http://",
                 "https://"))  # Check auf Schema hinzugefuegt

            # Wenn der Vorschlag U wie eine Wikipedia-URL aussieht
            if condition1_u_is_wiki_url:
                new_url = vorschlag_u  # Nehme den Vorschlag als potenzielle neue URL
                # Kriterium 2: Unterscheidet sich der Vorschlag U von der aktuellen URL in M?
                # Pruefe, ob die neue URL nicht identisch mit der aktuellen
                # M-URL ist.
                condition2_u_differs_m = new_url != url_m

                # Wenn sich der Vorschlag U von der aktuellen M-URL
                # unterscheidet
                if condition2_u_differs_m:
                    self.logger.debug(
                        f"  -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...")
                    try:
                        # Nutze die globale Funktion 'is_valid_wikipedia_article_url' (definiert in Block 12)
                        # Diese Funktion ist bereits mit @retry_on_failure
                        # dekoriert.
                        condition3_u_is_valid = is_valid_wikipedia_article_url(new_url, lang=getattr(
                            Config, 'LANG', 'de'))  # lang Argument hinzugefügt für Konsistenz

                        if condition3_u_is_valid:
                            is_update_candidate = True
                            self.logger.debug(
                                f"  -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.")  # <<< GEÄNDERT
                        else:
                            # Wenn die vorgeschlagene URL nicht valide ist
                            self.logger.debug(
                                f"  -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.")  # <<< GEÄNDERT

                    except Exception as e_validity_check:
                        # Wenn die Validierungsfunktion eine Exception wirft (nach Retries)
                        # Der Fehler wird bereits vom retry_on_failure
                        # Decorator geloggt.
                        self.logger.error(
                            f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}")  # <<< GEÄNDERT
                        # Bei Fehler bleibt is_update_candidate False.
                        pass  # Faert fort

                else:
                    # Wenn der Vorschlag U identisch mit der aktuellen M-URL
                    # ist
                    self.logger.debug(
                        f"  -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.")  # <<< GEÄNDERT

            else:
                # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht
                self.logger.debug(
                    f"  -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.")  # <<< GEÄNDERT

            # --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags ---
            updates_for_row = []  # Lokale Liste fuer Updates DIESER Zeile

            if is_update_candidate:
                # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M
                # kopiert)
                self.logger.info(
                    f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.")  # <<< GEÄNDERT
                updated_url_count += 1  # Zaehle die uebernommene URL

                # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt
                # interne Helfer _get_col_letter Block 14)
                # Setze die neue URL in Spalte M (Block 1 Column Map)
                updates_for_row.append(
                    {'range': f'{m_letter}{i}', 'values': [[new_url]]})
                # Setze Status S auf "X (URL Copied)" (Block 1 Column Map)
                updates_for_row.append(
                    {'range': f'{s_letter}{i}', 'values': [["X (URL Copied)"]]})
                # Schreibe Info in Spalte U (Block 1 Column Map)
                updates_for_row.append(
                    {'range': f'{u_letter}{i}', 'values': [["URL uebernommen"]]})
                # Setze ReEval Flag (A) auf 'x' (Block 1 Column Map)
                updates_for_row.append(
                    {'range': f'{a_letter}{i}', 'values': [["x"]]})

                # Leere Spalten N-V.
                # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der
                # Bereichsname ermittelt werden konnte.
                if nv_range_letter:  # Pruefe, ob der Bereichsname ermittelt werden konnte.
                    updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [
                                           empty_nv_values]})  # Block 1 Column Map, lokale Variable
                else:
                    self.logger.warning(
                        f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.")  # <<< GEÄNDERT

                # Leere Timestamps AN, AO, AP, AX, AY.
                # Dies setzt die Zeile zurueck, damit andere Schritte sie
                # spaeter bearbeiten.
                # AN (Wiki Extraction TS) Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{an_letter}{i}', 'values': [['']]})
                # AO (Chat Evaluation TS) Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ao_letter}{i}', 'values': [['']]})
                # AP (Version) Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ap_letter}{i}', 'values': [['']]})
                # AX (Wiki Verif. TS) Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ax_letter}{i}', 'values': [['']]})
                # AY (SerpAPI Wiki TS) Block 1 Column Map
                updates_for_row.append(
                    {'range': f'{ay_letter}{i}', 'values': [['']]})

            else:
                # Fall 2: Ungueltigen Vorschlag loeschen/markieren
                # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig
                # oder identisch mit M).
                self.logger.info(
                    f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.")  # <<< GEÄNDERT
                cleared_suggestion_count += 1  # Zaehle den bereinigten Vorschlag

                # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter
                # Block 14)
                # Setze Status S auf "X (Invalid Suggestion)" (Block 1 Column
                # Map)
                updates_for_row.append(
                    {'range': f'{s_letter}{i}', 'values': [["X (Invalid Suggestion)"]]})
                # Loesche den Vorschlag in Spalte U (Block 1 Column Map)
                updates_for_row.append(
                    {'range': f'{u_letter}{i}', 'values': [[""]]})
                # KEIN ReEval-Flag (A) setzen in diesem Fall.

            # Sammle die Updates fuer diese Zeile in der globalen Liste
            # all_sheet_updates.
            all_sheet_updates.extend(updates_for_row)

            # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist.
            # update_batch_row_limit wird aus Config geholt (Block 1).
            # Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem).
            # Pruefen Sie einfach die Laenge der gesammelten Liste.
            if len(all_sheet_updates) >= update_batch_row_limit * \
                    5:  # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile
                self.logger.debug(
                    f"  Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
                # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
                # Wenn es fehlschlaegt, wird es intern geloggt.
                success = self.sheet_handler.batch_update_cells(
                    all_sheet_updates)
                if success:
                    self.logger.info(
                        f"  Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")  # <<< GEÄNDERT
                # Der Fehlerfall wird von batch_update_cells geloggt

                # Leere die gesammelten Updates nach dem Senden.
                all_sheet_updates = []

            # Kleine Pause nach jeder geprueften Zeile (nutzt Config Block 1).
            # Dieser Modus macht API calls (ueber
            # is_valid_wikipedia_article_url), also Pause einbauen.
            pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2
            # self.logger.debug(f"Warte {pause_duration:.2f}s nach
            # Pruefung...") # Zu viel Laerm im Debug
            time.sleep(pause_duration)

        # --- Finale Sheet Updates senden ---
        # Sende alle verbleibenden gesammelten Updates in einem letzten
        # Batch-Update.
        if all_sheet_updates:
            self.logger.info(
                f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")  # <<< GEÄNDERT
            # Nutzt die batch_update_cells Methode des Sheet Handlers (Block
            # 14) mit Retry.
            success = self.sheet_handler.batch_update_cells(all_sheet_updates)
            if success:
                # <<< GEÄNDERT
                self.logger.info(f"FINALES Sheet-Update erfolgreich.")
            # Der Fehlerfall wird von batch_update_cells geloggt

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.")  # <<< GEÄNDERT

    def process_wiki_reextract_missing_an(
            self,
            start_sheet_row=None,
            end_sheet_row=None,
            limit=None):
        """
        Startet eine erneute Wiki-Extraktion für Zeilen mit URL aber ohne Timestamp.
        """
        """
        Identifiziert Zeilen, bei denen eine Wiki URL (M) vorhanden ist, aber der
        Wikipedia Timestamp (AN) fehlt. Fuehrt _process_single_row fuer diese Zeilen aus,
        beschraenkt auf den 'wiki'-Schritt und mit force_reeval=True, um die Extraktion
        erneut zu versuchen.

        Args:
            start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AN).
            end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
            limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt).
        """
        # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
        # Logge die Konfiguration des Modus
        self.logger.info(
            f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")  # <<< GEÄNDERT

        # --- Daten laden und Startzeile ermitteln ---
        # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt.
        # Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile
        # basierend auf leeren AN ist ein guter Startpunkt.
        if start_sheet_row is None:
            self.logger.info(
                "Automatische Ermittlung der Startzeile basierend auf leeren AN...")  # <<< GEÄNDERT
            # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map).
            # Standardmaessig ab Zeile 7
            start_data_index_no_header = self.sheet_handler.get_start_row_index(
                check_column_key="Wikipedia Timestamp", min_sheet_row=7)

            # Wenn get_start_row_index -1 zurueckgibt (Fehler)
            if start_data_index_no_header == -1:
                self.logger.error(
                    "FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.")  # <<< GEÄNDERT
                return  # Beende die Methode

            # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten
            # Daten-Index
            start_sheet_row = start_data_index_no_header + \
                self.sheet_handler._header_rows + 1  # Block 14 SheetHandler Attribut
            self.logger.info(
                f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}")  # <<< GEÄNDERT
        else:
            # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
            # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block
            # 2).
            if not self.sheet_handler.load_data():
                self.logger.error(
                    "Fehler beim Laden der Daten fuer wiki_reextract_missing_an.")  # <<< GEÄNDERT
                return  # Beende die Methode, wenn das Laden fehlschlaegt

        # Holen Sie die gesamte Datenliste (inklusive Header) aus dem
        # SheetHandler.
        all_data = self.sheet_handler.get_all_data_with_headers()
        # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar
        # (Block 14).
        header_rows = self.sheet_handler._header_rows
        total_sheet_rows = len(all_data)  # Gesamtzahl der Zeilen im Sheet

        # Berechne Endzeile, wenn nicht manuell gesetzt
        if end_sheet_row is None:
            end_sheet_row = total_sheet_rows  # Bis zur letzten Zeile

        # Logge den verarbeitungsbereich
        self.logger.info(
            f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")  # <<< GEÄNDERT

        # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht
        # ueber Gesamtzeilen)
        if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
            self.logger.info(
                "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")  # <<< GEÄNDERT
            return  # Beende die Methode, wenn der Bereich leer ist

        # --- Indizes ---
        # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP
        # (Block 1) vorhanden sind
        required_keys = ["Wiki URL", "Wikipedia Timestamp",
                         "CRM Name"]  # M, AN, B (Pruefkriterien + Logging)
        # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
        col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}

        # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden
        # wurden
        if None in col_indices.values():
            missing = [k for k, v in col_indices.items() if v is None]
            self.logger.critical(
                f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.")  # <<< GEÄNDERT
            return  # Beende die Methode bei kritischem Fehler

        # Ermitteln Sie die Indizes
        m_col_idx = col_indices["Wiki URL"]
        an_col_idx = col_indices["Wikipedia Timestamp"]

        # --- Verarbeitung ---
        # Zaehlt Zeilen, die an _process_single_row uebergeben wurden (im
        # Rahmen des Limits zaehlen).
        processed_count = 0
        skipped_count = 0  # Zaehlt Zeilen, die uebersprungen wurden.

        # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte
        # Sheet-Zeilennummer)
        for i in range(start_sheet_row, end_sheet_row + 1):
            row_index_in_list = i - 1  # 0-basierter Index in der all_data Liste
            # Pruefen Sie, ob das Ende des Sheets erreicht wurde
            if row_index_in_list >= total_sheet_rows:
                break  # Ende des Sheets erreicht

            row = all_data[row_index_in_list]  # Die Rohdaten fuer diese Zeile

            # Stellen Sie sicher, dass die Zeile nicht leer ist
            if not any(cell and isinstance(cell, str) and cell.strip()
                       for cell in row):
                # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).")
                # # Zu viel Laerm im Debug
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
            # Kriterium: Wiki URL (M) ist vorhanden und gueltig aussehend.
            # UND Wikipedia Timestamp (AN) ist leer.

            # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne
            # Helfer _get_cell_value_safe)
            m_value = self._get_cell_value_safe(
                row, "Wiki URL").strip()  # Block 1 Column Map
            an_value = self._get_cell_value_safe(
                row, "Wikipedia Timestamp").strip()  # Block 1 Column Map

            # Pruefen Sie, ob M gefuellt und gueltig aussieht.
            is_m_valid_looking = m_value and isinstance(
                m_value,
                str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in [
                "k.a.",
                "kein artikel gefunden",
                "fehler bei suche",
                "http:"]  # Fuege "http:" hinzu basierend auf Log

            # Pruefen Sie, ob AN leer ist.
            is_an_empty = not an_value

            # Verarbeitung ist noetig, wenn M gueltig aussieht UND AN leer ist.
            processing_needed_for_row = is_m_valid_looking and is_an_empty

            # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
            log_check = (
                i < start_sheet_row +
                5) or (
                i %
                100 == 0) or (processing_needed_for_row)
            if log_check:
                company_name = self._get_cell_value_safe(
                    row, "CRM Name").strip()  # Block 1 Column Map
                self.logger.debug(
                    f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}")  # <<< GEÄNDERT

            # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
            if not processing_needed_for_row:
                skipped_count += 1  # Zaehlen als uebersprungene Zeile
                continue  # Springe zur naechsten Zeile

            # --- Wenn Verarbeitung noetig: Rufe _process_single_row auf ---
            # Zaehle die Zeile, die an _process_single_row uebergeben wird (im
            # Rahmen des Limits zaehlen)
            processed_count += 1

            # Pruefe das Limit fuer verarbeitete Zeilen
            if limit is not None and isinstance(
                    limit, int) and limit > 0 and processed_count > limit:
                # Wenn das Limit erreicht ist und es ein positives Limit gibt
                self.logger.info(
                    f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.")  # <<< GEÄNDERT
                break  # Brich die Schleife ab

            self.logger.info(
                f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...")  # <<< GEÄNDERT

            try:
                # RUFE _process_single_row AUF (Block 19).
                # Mit steps_to_run={'wiki'} und force_reeval=True,
                # damit nur der Wiki-Schritt ausgefuehrt wird und Timestamps ignoriert werden.
                # Im Re-Extract Modus loeschen wir das 'x'-Flag NICHT
                # automatisch.
                self._process_single_row(
                    row_num_in_sheet=i,
                    row_data=row,  # Uebergibt die aktuellen Rohdaten der Zeile
                    steps_to_run={'wiki'},
                    # <<< NUR der Wiki-Schritt soll laufen
                    force_reeval=True,
                    # <<< Erzwingt die Ausfuehrung des 'wiki' Schritts (ignoriert AN, S).
                    clear_x_flag=False  # <<< 'x'-Flag wird in diesem Modus NICHT geloescht
                )
                # _process_single_row (Block 19) loggt intern den Abschluss und
                # fuehrt das Sheet-Update durch.

            except Exception as e_proc:
                # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
                # fangen wir ihn hier, loggen ihn und fahren mit der naechsten
                # Zeile fort.
                self.logger.exception(
                    f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}")  # <<< GEÄNDERT
                # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
                # Dieses Update muesste dann separat oder im naechsten Lauf
                # behandelt werden.

            # _process_single_row beinhaltet bereits eine kleine Pause am Ende.
            # Hier ist keine zusaetzliche Pause noetig, wenn _process_single_row erfolgreich war.
            # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein.
            # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception

        # Logge den Abschluss des Modus
        self.logger.info(
            f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.")  # <<< GEÄNDERT
