import logging
import os
import subprocess

try:
    result = subprocess.run(['bash', 'install.sh'], check=True)
except subprocess.CalledProcessError as e:
    print(f"The install script failed with return code {e.returncode}")

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

import gradio as gr
import torah
import bible
import quran
import hindu
import tripitaka
from utils import number_to_ordinal_word, custom_normalize, date_to_words, translate_date_to_words
from gematria import calculate_gematria, strip_diacritics

import pandas as pd
from deep_translator import GoogleTranslator
from gradio_calendar import Calendar
from datetime import datetime, timedelta
import math
import json
import re
import sqlite3
from collections import defaultdict
from typing import List, Tuple
# import rich  # Removed rich
# from fuzzywuzzy import fuzz  # Removed fuzzywuzzy
import calendar
import translation_utils
import hashlib
import copy
from collections import Counter

translation_utils.create_translation_table()

# Create a translator instance *once* globally
translator = GoogleTranslator(source='auto', target='auto')
LANGUAGES_SUPPORTED = translator.get_supported_languages(as_dict=True)

LANGUAGE_CODE_MAP = LANGUAGES_SUPPORTED  # Use deep_translator's mapping directly

# --- Constants ---
DATABASE_FILE = 'gematria.db'
MAX_PHRASE_LENGTH_LIMIT = 20

ELS_CACHE_DB = "els_cache.db"
DATABASE_TIMEOUT = 60

# --- ELS Cache Functions ---
def create_els_cache_table():
    if not os.path.exists(ELS_CACHE_DB):
        with sqlite3.connect(ELS_CACHE_DB) as conn:
            conn.execute('''
                CREATE TABLE IF NOT EXISTS els_cache (
                    query_hash TEXT PRIMARY KEY,
                    function_name TEXT,
                    args TEXT,
                    kwargs TEXT,
                    results TEXT
                )
            ''')

# --- Database Initialization ---
def initialize_database():
    global conn
    conn = sqlite3.connect(DATABASE_FILE)
    cursor = conn.cursor()
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS results (
        gematria_sum INTEGER,
        words TEXT,
        translation TEXT,
        book TEXT,
        chapter INTEGER,
        verse INTEGER,
        phrase_length INTEGER,
        word_position TEXT,
        PRIMARY KEY (gematria_sum, words, book, chapter, verse, word_position)
    )
    ''')
    cursor.execute('''
    CREATE INDEX IF NOT EXISTS idx_results_gematria
    ON results (gematria_sum)
    ''')
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS processed_books (
        book TEXT PRIMARY KEY,
        max_phrase_length INTEGER
    )
    ''')
    conn.commit()

# --- Initialize Database ---
initialize_database()

# --- ELS Cache Functions ---
def create_els_cache_table():
    with sqlite3.connect(ELS_CACHE_DB) as conn:
        try:
            conn.execute('''
                CREATE TABLE IF NOT EXISTS els_cache (
                    query_hash TEXT PRIMARY KEY,
                    function_name TEXT,
                    args TEXT,
                    kwargs TEXT,
                    results TEXT
                )
            ''')
        except sqlite3.OperationalError as e:
            logger.error(f"Error creating table: {e}")

def get_query_hash(func, args, kwargs):
    key = (func.__name__, args, kwargs)
    return hashlib.sha256(json.dumps(key).encode()).hexdigest()

def cached_process_json_files(func, *args, **kwargs):
    # Create a dictionary to store the parameters
    params = {
        "function": f"{func.__module__}.{func.__name__}"
    }

    # Add the positional arguments with their names
    arg_names = func.__code__.co_varnames[:func.__code__.co_argcount]
    for name, value in zip(arg_names, args):
        params[name] = value

    # Add the keyword arguments
    for name, value in kwargs.items():
        params[name] = value

    # Convert the parameters to a JSON string
    params_json = json.dumps(params)

    # Use the parameters JSON string to generate the query hash
    query_hash = get_query_hash(func, params_json, "")

    # Ensure the table exists before any operations
    create_els_cache_table()

    try:
        with sqlite3.connect(ELS_CACHE_DB, timeout=DATABASE_TIMEOUT) as conn:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT results FROM els_cache WHERE query_hash = ?", (query_hash,))
            result = cursor.fetchone()
            if result:
                logger.info(f"Cache hit for query: {query_hash}")
                return json.loads(result[0])
    except sqlite3.Error as e:
        logger.error(f"Database error checking cache: {e}")

    logger.info(f"Cache miss for query: {query_hash}")
    results = func(*args, **kwargs)

    try:
        with sqlite3.connect(ELS_CACHE_DB, timeout=DATABASE_TIMEOUT) as conn:
            cursor = conn.cursor()
            cursor.execute(
                "INSERT INTO els_cache (query_hash, function_name, args, kwargs, results) VALUES (?, ?, ?, ?, ?)",
                (query_hash, params["function"], params_json, json.dumps({}), json.dumps(results)))
            conn.commit()
    except sqlite3.Error as e:
        logger.error(f"Database error caching results: {e}")

    return results

# --- Helper Functions (from Network app.py) ---
def flatten_text(text: List) -> str:
    if isinstance(text, list):
        return " ".join(flatten_text(item) if isinstance(item, list) else item for item in text)
    return text

def search_gematria_in_db(gematria_sum: int, max_words: int) -> List[Tuple[str, str, int, int, int, str]]:
    global conn
    with sqlite3.connect(DATABASE_FILE) as conn:
        cursor = conn.cursor()
        cursor.execute('''
        SELECT words, book, chapter, verse, phrase_length, word_position
        FROM results
        WHERE gematria_sum = ? AND phrase_length <= ?
        ''', (gematria_sum, max_words))
        results = cursor.fetchall()
    return results

def get_most_frequent_phrase(results):
    phrase_counts = defaultdict(int)
    for words, book, chapter, verse, phrase_length, word_position in results:
        phrase_counts[words] += 1
    most_frequent_phrase = max(phrase_counts, key=phrase_counts.get) if phrase_counts else None
    return most_frequent_phrase

# --- Functions from BOS app.py ---
def create_language_dropdown(label, default_value='English', show_label=True):
    return gr.Dropdown(
        choices=list(LANGUAGE_CODE_MAP.keys()),
        label=label,
        value=default_value,
        show_label=show_label
    )

def calculate_gematria_sum(text, date_words):
    if text or date_words:
        combined_input = f"{text} {date_words}"
        logger.info(f"searching for input: {combined_input}")
        numbers = re.findall(r'\d+', combined_input)
        text_without_numbers = re.sub(r'\d+', '', combined_input)
        number_sum = sum(int(number) for number in numbers)
        text_gematria = calculate_gematria(strip_diacritics(text_without_numbers))
        total_sum = text_gematria + number_sum
        return total_sum
    else:
        return None

def perform_els_search(step, rounds_combination, tlang, strip_spaces, strip_in_braces, strip_diacritics_chk,
                       include_torah, include_bible, include_quran, include_hindu, include_tripitaka):
    if step == 0 or rounds_combination == "0,0":
        return None

    results = {}
    length = 0

    selected_language_long = tlang
    tlang = LANGUAGES_SUPPORTED.get(selected_language_long)
    if tlang is None:
        tlang = "en"
        logger.warning(
            f"Unsupported language selected: {selected_language_long}. Defaulting to English (en).")

    if include_torah:
        logger.debug(
            f"Arguments for Torah: {(1, 39, step, rounds_combination, length, tlang, strip_spaces, strip_in_braces, strip_diacritics_chk)}")
        results["Torah"] = cached_process_json_files(torah.process_json_files, 1, 39, step, rounds_combination, length,
                                                     tlang, strip_spaces, strip_in_braces, strip_diacritics_chk)
    else:
        results["Torah"] = []

    if include_bible:
        results["Bible"] = cached_process_json_files(bible.process_json_files, 40, 66, step, rounds_combination,
                                                     length,
                                                     tlang, strip_spaces, strip_in_braces, strip_diacritics_chk)
    else:
        results["Bible"] = []

    if include_quran:
        results["Quran"] = cached_process_json_files(quran.process_json_files, 1, 114, step, rounds_combination,
                                                     length,
                                                     tlang, strip_spaces, strip_in_braces, strip_diacritics_chk)
    else:
        results["Quran"] = []

    if include_hindu:
        results["Rig Veda"] = cached_process_json_files(
            hindu.process_json_files, 1, 10, step, rounds_combination, length, tlang, False, strip_in_braces,
            strip_diacritics_chk)
    else:
        results["Rig Veda"] = []

    if include_tripitaka:
        results["Tripitaka"] = cached_process_json_files(
            tripitaka.process_json_files, 1, 52, step, rounds_combination, length, tlang, strip_spaces,
            strip_in_braces, strip_diacritics_chk)
    else:
        results["Tripitaka"] = []

    return results

def add_24h_projection(results_dict):
    for book_name, results in results_dict.items():
        num_results = len(results)
        if num_results > 0:
            time_interval = timedelta(minutes=24 * 60 / num_results)
            current_time = datetime.min.time()
            for i in range(num_results):
                next_time = (datetime.combine(datetime.min, current_time) + time_interval).time()
                time_range_str = f"{current_time.strftime('%H:%M')}-{next_time.strftime('%H:%M')}"
                results[i]['24h Projection'] = time_range_str
                current_time = next_time
    return results_dict

def add_monthly_projection(results_dict, selected_date):
    if selected_date is None:
        return results_dict

    for book_name, results in results_dict.items():
        num_results = len(results)
        if num_results > 0:
            days_in_month = calendar.monthrange(selected_date.year, selected_date.month)[1]
            total_seconds = (days_in_month - 1) * 24 * 3600
            seconds_interval = total_seconds / num_results
            start_datetime = datetime(selected_date.year, selected_date.month, 1)
            current_datetime = start_datetime

            for i in range(num_results):
                next_datetime = current_datetime + timedelta(seconds=seconds_interval)
                current_date = current_datetime.date()
                next_date = next_datetime.date()
                date_range_str = f"{current_date.strftime('%h %d')} - {next_date.strftime('%h %d')}"
                results[i]['Monthly Projection'] = date_range_str
                current_datetime = next_datetime
                current_date = next_datetime.date()
    return results_dict

def add_yearly_projection(results_dict, selected_date):
    if selected_date is None:
        return results_dict

    for book_name, results in results_dict.items():
        num_results = len(results)
        if num_results > 0:
            days_in_year = 366 if calendar.isleap(selected_date.year) else 365
            total_seconds = (days_in_year - 1) * 24 * 3600
            seconds_interval = total_seconds / num_results
            start_datetime = datetime(selected_date.year, 1, 1)
            current_datetime = start_datetime

            for i in range(num_results):
                next_datetime = current_datetime + timedelta(seconds=seconds_interval)
                current_date = current_datetime.date()
                next_date = next_datetime.date()
                date_range_str = f"{current_date.strftime('%b %d')} - {next_date.strftime('%b %d')}"
                results[i]['Yearly Projection'] = date_range_str
                current_datetime = next_datetime

    return results_dict

def sort_results(results):
    def parse_time(time_str):
        try:
            hours, minutes = map(int, time_str.split(':'))
            return hours * 60 + minutes
        except ValueError:
            return 24 * 60

    return sorted(results, key=lambda x: (
        parse_time(x.get('24h Projection', '23:59').split('-')[0]),
        parse_time(x.get('24h Projection', '23:59').split('-')[1])
    ))

def extract_rounds_combinations():
    """Extracts unique rounds combinations from the database."""
    combinations = set()
    try:
        with sqlite3.connect(ELS_CACHE_DB) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT args FROM els_cache")
            all_args = cursor.fetchall()
            for args_tuple in all_args:
                args_str = args_tuple[0]
                try:
                    args_json = json.loads(args_str)
                    if 'rounds' in args_json:
                        combinations.add(args_json['rounds'])
                except json.JSONDecodeError:
                    logger.error(f"Could not decode JSON for args: {args_str}")
    except sqlite3.Error as e:
        logger.error(f"Database error: {e}")
    logger.info(f"Found unique rounds combinations: {combinations}")
    return ["All"] + sorted(list(combinations))

def update_rounds_dropdown():
    new_choices = extract_rounds_combinations()
    return new_choices

def perform_gematria_calculation_for_date_range(start_date, end_date):
    logger.debug(f"Calculating date gematria for range: {start_date} - {end_date}")
    results = {}
    delta = timedelta(days=1)
    current_date = start_date

    while current_date <= end_date:
        date_string = current_date.strftime("%Y-%m-%d")
        date_words = date_to_words(date_string)
        date_gematria = calculate_gematria_sum(date_words, "")  # Angepasst, um der Funktion calculate_gematria_sum zu entsprechen

        results[date_string] = {
            "date_words": date_words,
            "date_gematria": date_gematria,
        }
        current_date += delta
    logger.debug(f"Finished calculating date gematria.")
    return results

def find_matching_dates(date_gematrias, names, search_journal_sum):
    logger.debug(f"Searching for matches with journal sum: {search_journal_sum}")
    matching_dates = {}

    for name in names:
        name_gematria = calculate_gematria_sum(name, "") # Angepasst, um der Funktion calculate_gematria_sum zu entsprechen
        target_date_gematria = search_journal_sum - name_gematria if name_gematria is not None else None
        logger.debug(f"Name: {name}, Gematria: {name_gematria}, Target Date Gematria: {target_date_gematria}")

        if target_date_gematria is not None:
            for date_str, date_data in date_gematrias.items():
                if date_data["date_gematria"] == target_date_gematria:
                    if name not in matching_dates:
                        matching_dates[name] = []
                    matching_dates[name].append(date_str)
        logger.debug(f"Matches for {name}: {matching_dates.get(name, [])}")
    return matching_dates

def find_shared_journal_sums(date_gematrias, names):
    """Finds shared journal sums and formats output with names and dates together."""
    logger.debug("Calculating shared journal sums...")
    shared_sums = {}
    name_gematrias = {name: calculate_gematria_sum(name, "") for name in names}

    for date_str, date_data in date_gematrias.items():
        date_gematria = date_data["date_gematria"]
        for name, name_gematria in name_gematrias.items():
            journal_sum = date_gematria + name_gematria
            journal_sum_str = str(journal_sum) # Konvertiere den Schlüssel (journal_sum) in einen String
            if journal_sum_str not in shared_sums:
                shared_sums[journal_sum_str] = {}
            if name not in shared_sums[journal_sum_str]:
                shared_sums[journal_sum_str][name] = []
            shared_sums[journal_sum_str][name].append(date_str)

    # Filter out sums not shared by at least two names and format output
    result = {}
    for journal_sum_str, data in shared_sums.items():
        if len(data) >= 2:
            result[journal_sum_str] = {}
            for name, dates in data.items():
                result[journal_sum_str][name] = dates

    logger.debug(f"Shared Journal Sums: {result}")
    return result

def calculate_and_find_dates(start_date, end_date, names_input, search_journal_sum, find_shared=False):
    names = [n.strip() for n in names_input.split("\n") if n.strip()]
    date_gematrias = perform_gematria_calculation_for_date_range(start_date, end_date)
    if find_shared:
        shared_sums = find_shared_journal_sums(date_gematrias, names)
        return None, shared_sums
    else:
        matching_dates = find_matching_dates(date_gematrias, names, int(search_journal_sum))
        return matching_dates, None


# --- Main Gradio App ---
with gr.Blocks() as app:
    with gr.Tab("ELS Search"):
        with gr.Column():
            with gr.Row():
                tlang = create_language_dropdown("Target Language for Result Translation", default_value='english')
                selected_date = Calendar(type="datetime", label="Date to investigate (optional)",
                                         info="Pick a date from the calendar")
                use_day = gr.Checkbox(label="Use Day", info="Check to include day in search", value=True)
                use_month = gr.Checkbox(label="Use Month", info="Check to include month in search", value=True)
                use_year = gr.Checkbox(label="Use Year", info="Check to include year in search", value=True)
                date_language_input = create_language_dropdown(
                    "Language of the person/topic (optional) (Date Word Language)", default_value='english')
            with gr.Row():
                gematria_text = gr.Textbox(label="Name and/or Topic (required)",
                                            value="Albert Einstein Mileva Marity-Einstein")
                date_words_output = gr.Textbox(label="Date in Words Translated (optional)")
                gematria_result = gr.Number(label="Journal Sum")
            # with gr.Row():

            with gr.Row():
                step = gr.Number(label="Jump Width (Steps) for ELS")
                float_step = gr.Number(visible=False, value=1)
                half_step_btn = gr.Button("Steps / 2")
                double_step_btn = gr.Button("Steps * 2")

                with gr.Column():
                    round_x = gr.Number(label="Round (1)", value=1)
                    round_y = gr.Number(label="Round (2)", value=-1)

                rounds_combination = gr.Textbox(label="Combined Rounds", value="1,-1")

            with gr.Row():
                include_torah_chk = gr.Checkbox(label="Include Torah", value=True)
                include_bible_chk = gr.Checkbox(label="Include Bible", value=True)
                include_quran_chk = gr.Checkbox(label="Include Quran", value=True)
                include_hindu_chk = gr.Checkbox(label="Include Rigveda", value=False)
                include_tripitaka_chk = gr.Checkbox(label="Include Tripitaka", value=False)

                strip_spaces = gr.Checkbox(label="Strip Spaces from Books", value=True)
                strip_in_braces = gr.Checkbox(label="Strip Text in Braces from Books", value=True)
                strip_diacritics_chk = gr.Checkbox(label="Strip Diacritics from Books", value=True)

            translate_btn = gr.Button("Search with ELS")

            # --- Output Components ---
            markdown_output = gr.Dataframe(label="ELS Results")
            most_frequent_phrase_output = gr.Textbox(label="Most Frequent Phrase in Network Search")
            json_output = gr.JSON(label="JSON Output")

    with gr.Tab("Cache Database Search"):
        with gr.Column():
            with gr.Row():
                main_book_filter = gr.Dropdown(label="Filter by Main Book",
                                            choices=["All", "Torah", "Bible", "Quran", "Rig Veda", "Tripitaka"],
                                            value="Torah")
                # Keine choices hier, nur das Label und den Initialwert
                rounds_filter = gr.Dropdown(label="Filter by Rounds", allow_custom_value=True, value="1,-1")

            with gr.Row():
                search_type = gr.Radio(label="Search by",
                                    choices=["Text in result_text", "Gematria Sum in results"],
                                    value="Text in result_text")
            with gr.Row():
                search_mode = gr.Radio(label="Search Mode",
                                    choices=["Exact Search", "Contains Word"],
                                    value="Contains Word")

            with gr.Row():
                search_term = gr.Textbox(label="Search Term", visible=True)
                gematria_sum_search = gr.Number(label="Gematria Sum", visible=False)

            with gr.Row():
                search_db_btn = gr.Button("Search Cache Database")
            with gr.Row():
                cache_search_results = gr.JSON(label="Cache Search Results")

        def update_search_components(search_type):
             if search_type == "Text in result_text":
                return gr.Textbox.update(visible=True), gr.Number.update(visible=False)
             else:
                return gr.Textbox.update(visible=False), gr.Number.update(visible=True)


        def search_cache_database(search_type, search_term, gematria_sum_search, main_book_filter, rounds_filter, search_mode):
            """Searches the cache database based on the selected filters and search term."""
            results = []
            search_term = strip_diacritics(search_term)
            if main_book_filter == "All" and rounds_filter == "All" and not search_term and not gematria_sum_search:
                return results

            try:
                with sqlite3.connect(ELS_CACHE_DB) as conn:
                    cursor = conn.cursor()

                    if search_type == "Text in result_text":
                        # Optimization: If only main_book_filter is selected, don't perform a full search
                        if main_book_filter != "All" and rounds_filter == "All" and not search_term:
                            return results

                        cursor.execute("SELECT * FROM els_cache")
                        all_results = cursor.fetchall()
                        columns = [desc[0] for desc in cursor.description]

                        for row in all_results:
                            row_dict = dict(zip(columns, row))
                            args_dict = json.loads(row_dict['args'])
                            function_name = row_dict['function_name']

                            # Function name filtering
                            include_result = False
                            if main_book_filter == "All":
                                include_result = True
                            elif main_book_filter == "Torah" and function_name == "torah.process_json_files":
                                include_result = True
                            elif main_book_filter == "Bible" and function_name == "bible.process_json_files":
                                include_result = True
                            elif main_book_filter == "Quran" and function_name == "quran.process_json_files":
                                include_result = True
                            elif main_book_filter == "Rig Veda" and function_name == "hindu.process_json_files":
                                include_result = True
                            elif main_book_filter == "Tripitaka" and function_name == "tripitaka.process_json_files":
                                include_result = True

                            if not include_result:
                                continue

                            # Rounds filtering
                            if rounds_filter != "All" and args_dict.get('rounds') != rounds_filter:
                                continue

                            try:
                                results_json = json.loads(row_dict['results'])
                                for result_entry in results_json:
                                    if 'result_text' in result_entry:
                                        if search_mode == "Exact Search" and search_term == result_entry['result_text']:
                                            entry = {
                                                'function_name': function_name,
                                                'step': args_dict.get('step'),
                                                'rounds': args_dict.get('rounds'),
                                                'result': result_entry
                                            }
                                            results.append(entry)
                                        elif search_mode == "Contains Word" and search_term in result_entry['result_text']:
                                            entry = {
                                                'function_name': function_name,
                                                'step': args_dict.get('step'),
                                                'rounds': args_dict.get('rounds'),
                                                'result': result_entry
                                            }
                                            results.append(entry)
                            except (json.JSONDecodeError, TypeError) as e:
                                logger.error(f"Error processing row: {e}")
                                continue

                    elif search_type == "Gematria Sum in results":

                        # Optimization: If only main_book_filter is selected, don't perform a full search
                        if main_book_filter != "All" and rounds_filter == "All" and not gematria_sum_search:
                            return results

                        if not isinstance(gematria_sum_search, (int, float)):
                            return results

                        cursor.execute("SELECT * FROM els_cache")
                        all_results = cursor.fetchall()
                        columns = [desc[0] for desc in cursor.description]

                        for row in all_results:
                            row_dict = dict(zip(columns, row))
                            args_dict = json.loads(row_dict['args'])
                            function_name = row_dict['function_name']

                            # Function name filtering
                            include_result = False
                            if main_book_filter == "All":
                                include_result = True
                            elif main_book_filter == "Torah" and function_name == "torah.process_json_files":
                                include_result = True
                            elif main_book_filter == "Bible" and function_name == "bible.process_json_files":
                                include_result = True
                            elif main_book_filter == "Quran" and function_name == "quran.process_json_files":
                                include_result = True
                            elif main_book_filter == "Rig Veda" and function_name == "hindu.process_json_files":
                                include_result = True
                            elif main_book_filter == "Tripitaka" and function_name == "tripitaka.process_json_files":
                                include_result = True

                            if not include_result:
                                continue

                            # Rounds filtering
                            if rounds_filter != "All" and args_dict.get('rounds') != rounds_filter:
                                continue

                            try:
                                results_json = json.loads(row_dict['results'])
                                for result_entry in results_json:
                                    if 'result_sum' in result_entry and result_entry[
                                        'result_sum'] == gematria_sum_search:
                                        entry = {
                                            'function_name': function_name,
                                            'step': args_dict.get('step'),
                                            'rounds': args_dict.get('rounds'),
                                            'result': result_entry
                                        }
                                        results.append(entry)
                            except (json.JSONDecodeError, TypeError) as e:
                                logger.error(f"Error processing row: {e}")
                                continue

                # Sort results by gematria sum
                results.sort(
                    key=lambda x: x['result']['result_sum'] if 'result' in x and 'result_sum' in x['result'] else 0)
                return results

            except sqlite3.Error as e:
                logger.error(f"Database error: {e}")
                return []

        def update_search_components(search_type):
            """Updates the visibility of the search term and gematria sum input fields."""
            if search_type == "Text in result_text":
                return {"visible": True, "__type__": "update"}, {"visible": False, "__type__": "update"}
            else:
                return {"visible": False, "__type__": "update"}, {"visible": True, "__type__": "update"}

    with gr.Tab("Date Range Synchronicity + Journal-sum Search"):
        with gr.Row():
            start_date_jr = Calendar(type="datetime", label="Start Date")
            end_date_jr = Calendar(type="datetime", label="End Date")
        with gr.Row():
            names_input_jr = gr.Textbox(label="Names (one per line)", lines=5)
            search_sum_jr = gr.Number(label="Search Journal Sum", precision=0)

        with gr.Row():
            calculate_btn_jr = gr.Button("Search Journal Sum")
            shared_sums_btn_jr = gr.Button("Find Shared Journal Sums")

        matching_dates_output_jr = gr.JSON(label="Matching Dates")
        shared_sums_output_jr = gr.JSON(label="Shared Journal Sums")

        calculate_btn_jr.click(
            lambda start_date, end_date, names_input, search_sum: calculate_and_find_dates(
                start_date, end_date, names_input, search_sum, find_shared=False),
            inputs=[start_date_jr, end_date_jr, names_input_jr, search_sum_jr],
            outputs=[matching_dates_output_jr, shared_sums_output_jr]
        )

        shared_sums_btn_jr.click(
            lambda start_date, end_date, names_input: calculate_and_find_dates(
                start_date, end_date, names_input, 0, find_shared=True),
            inputs=[start_date_jr, end_date_jr, names_input_jr],
            outputs=[matching_dates_output_jr, shared_sums_output_jr]
        )


    with gr.Tab("Date Range ELS Journal Search"):
        with gr.Row():
            start_date_els = Calendar(type="datetime", label="Start Date")
            end_date_els = Calendar(type="datetime", label="End Date")
        with gr.Row():
            names_input_els = gr.Textbox(label="Names (one per line)", lines=5)
        with gr.Row():
            search_type_els = gr.Radio(
                label="Search by",
                choices=["Text in result_text", "Gematria Sum in results"],
                value="Text in result_text"
            )
        with gr.Row():
            search_mode_els = gr.Radio(
                label="Search Mode",
                choices=["Exact Search", "Contains Word"],
                value="Contains Word"
            )
        with gr.Row():
            search_term_els = gr.Textbox(label="Search Term", visible=True)
            gematria_sum_search_els = gr.Number(label="Gematria Sum", visible=False)
        with gr.Row():
            include_torah_chk_els = gr.Checkbox(label="Include Torah", value=True)
            include_bible_chk_els = gr.Checkbox(label="Include Bible", value=True)
            include_quran_chk_els = gr.Checkbox(label="Include Quran", value=True)
            include_hindu_chk_els = gr.Checkbox(label="Include Rigveda", value=False)
            include_tripitaka_chk_els = gr.Checkbox(label="Include Tripitaka", value=False)
        with gr.Row():
            translate_results_chk_els = gr.Checkbox(label="Translate Results to English", value=False)
        with gr.Row():
            sub_oscillation_search_chk_els = gr.Checkbox(label="Search in Sub-Oscillations", value=False) # Neue Checkbox
            sub_oscillation_level_els = gr.Number(label="Sub-Oscillation Level (0 = off)", precision=0, value=1)

        with gr.Row():
            perform_search_btn_els = gr.Button("Perform Search")

        filtered_results_output_els = gr.JSON(label="Filtered Results")

        # Funktionen zur Aktualisierung der Sichtbarkeit der Sucheingabefelder
        def update_search_components_els(search_type):
            if search_type == "Text in result_text":
                return gr.Textbox(visible=True), gr.Number(visible=False)
            else:
                return gr.Textbox(visible=False), gr.Number(visible=True)

        search_type_els.change(
            fn=update_search_components_els,
            inputs=[search_type_els],
            outputs=[search_term_els, gematria_sum_search_els]
        )

        def perform_els_search_for_gematria_sum(
            gematria_sum,
            include_torah,
            include_bible,
            include_quran,
            include_hindu,
            include_tripitaka
        ):
            """
            Calls your actual ELS search function, returning a dict like:
            {
            "Torah": [ { "result_sum": ..., "result_text": ..., "source_language": ...}, ...],
            "Bible": [...],
            ...
            }
            """
            return perform_els_search(
                step=gematria_sum,
                rounds_combination="1,-0.5,0.5,-1",
                tlang="english",
                strip_spaces=True,
                strip_in_braces=True,
                strip_diacritics_chk=True,
                include_torah=include_torah,
                include_bible=include_bible,
                include_quran=include_quran,
                include_hindu=include_hindu,
                include_tripitaka=include_tripitaka
            )


        def perform_sub_oscillation_search(
            base_results_list,
            initial_gematria_sum,
            level,
            include_torah,
            include_bible,
            include_quran,
            include_hindu,
            include_tripitaka
        ):
            """
            Recursively populates 'subresults' for each base entry, up to the specified level.
            base_results_list: A list of dicts, each shaped like:
            {
                "book": "Torah",
                "result": {
                    "result_sum": ...,
                    "result_text": ...
                },
                "subresults": []
            }
            initial_gematria_sum: The base sum from day+name (or from prior recursion).
            level: how many sub-levels we still want to descend.
            include_XXX: booleans for controlling which texts to include.
            """

            if level <= 0:
                return base_results_list

            for base_entry in base_results_list:
                parent_sum = base_entry["result"]["result_sum"]
                combined_sum = initial_gematria_sum + parent_sum

                # Next-level search
                sub_search_results = perform_els_search_for_gematria_sum(
                    gematria_sum=combined_sum,
                    include_torah=include_torah,
                    include_bible=include_bible,
                    include_quran=include_quran,
                    include_hindu=include_hindu,
                    include_tripitaka=include_tripitaka
                )

                # Build child entries
                child_entries = []
                for book_name, res_list in sub_search_results.items():
                    for one_res in res_list:
                        child_entries.append({
                            "book": book_name,
                            "result": one_res,
                            "subresults": []
                        })

                # Attach to subresults
                base_entry["subresults"].extend(child_entries)

                # Recurse deeper
                perform_sub_oscillation_search(
                    base_entry["subresults"],
                    initial_gematria_sum,
                    level - 1,
                    include_torah,
                    include_bible,
                    include_quran,
                    include_hindu,
                    include_tripitaka
                )

            return base_results_list


        def matches_criteria(item, search_type, search_term, gematria_sum_search, search_mode):
            """
            Checks if a single item (a dict with 'result_text', 'result_sum', etc.)
            matches the user’s chosen filter criteria.

            item shape example:
            {
                "book": "Torah",
                "result": {
                "result_text": "foo bar",
                "result_sum": 1234,
                ...
                },
                "subresults": [ ... ]
            }
            """
            result_text = item["result"].get("result_text", "")
            result_sum  = item["result"].get("result_sum", None)

            if search_type == "Text in result_text":
                if search_mode == "Exact Search":
                    return (result_text == search_term)
                else:  # "Contains Word"
                    return (search_term in result_text)
            else:
                # search_type == "Gematria Sum in results"
                return (result_sum == gematria_sum_search)


        def prune_tree_by_search_criteria(results_list, search_type, search_term, gematria_sum_search, search_mode):
            """
            Recursively filters a list of items so that:
            - An item is included if it directly matches the search criteria,
                OR if any of its child subresults match (in which case we keep
                the item *and* the matching children).
            - This preserves parent chain for matching subresults.

            returns a new, pruned list of items with the same nested structure,
            but only containing the branches that match or lead to a match.
            """
            pruned_list = []
            for item in results_list:
                # Recurse into subresults first
                pruned_sub = prune_tree_by_search_criteria(
                    item["subresults"],
                    search_type,
                    search_term,
                    gematria_sum_search,
                    search_mode
                )
                does_item_match = matches_criteria(item, search_type, search_term, gematria_sum_search, search_mode)

                # If the item itself matches OR any child matches
                if does_item_match or len(pruned_sub) > 0:
                    # Make a copy of the item so we don't destroy the original
                    new_item = copy.deepcopy(item)
                    # Overwrite subresults with the pruned version
                    new_item["subresults"] = pruned_sub
                    pruned_list.append(new_item)

            return pruned_list



        def translate_subresults_recursive(subresults_list):
            """
            Recursively translates the 'result_text' of each item in subresults_list.
            We only visit items that remain in the filtered results.
            """
            for sub_item in subresults_list:
                text = sub_item["result"].get("result_text", "")
                source_lang = sub_item["result"].get("source_language", "auto")
                if text:
                    translated_text = translation_utils.get_translation(text, "en", source_lang)[0]
                    sub_item["result"]["translated_text"] = translated_text

                # Recurse into deeper subresults
                if sub_item["subresults"]:
                    translate_subresults_recursive(sub_item["subresults"])

        def perform_date_range_els_search(
            start_date,
            end_date,
            names_input,
            search_type,
            search_term,
            gematria_sum_search,
            search_mode,
            include_torah,
            include_bible,
            include_quran,
            include_hindu,
            include_tripitaka,
            translate_results,
            sub_oscillation_search,
            sub_oscillation_level
        ):
            """
            - Builds a fully nested sub-oscillation structure if requested.
            - Then prunes it so only matching items remain (plus parents).
            - Finally, optionally translates the pruned items if requested.
            - Returns the filtered (and optionally translated) structure.
            """
            # 1) Compute gematria sums for all dates in the chosen range
            names = [n.strip() for n in names_input.split("\n") if n.strip()]
            date_gematrias = perform_gematria_calculation_for_date_range(start_date, end_date)

            all_filtered_results = []

            # 2) Loop over each date
            for date_str, date_data in date_gematrias.items():
                for name in names:
                    # base sum: day gematria + name gematria
                    name_gem = calculate_gematria_sum(name, "")
                    initial_gem = date_data["date_gematria"] + name_gem
                    date_words = date_data["date_words"]
                    search_path_basic = f"{name} {date_words}"

                    # 3) Do the base ELS search (this is the top-level results)
                    base_results = perform_els_search_for_gematria_sum(
                        gematria_sum=initial_gem,
                        include_torah=include_torah,
                        include_bible=include_bible,
                        include_quran=include_quran,
                        include_hindu=include_hindu,
                        include_tripitaka=include_tripitaka
                    )

                    # 4) Convert "base_results" into a uniform nested format
                    #    shaped like { "0": {"book":..., "results": [ {...}, ... ]}, ... }
                    formatted_base_results = {}
                    idx_counter = 0
                    for book_name, book_res_list in base_results.items():
                        if book_res_list:
                            formatted_base_results[str(idx_counter)] = {
                                "book": book_name,
                                "results": []
                            }
                            for res in book_res_list:
                                formatted_base_results[str(idx_counter)]["results"].append({
                                    "book": book_name,
                                    "result": res,
                                    "subresults": []
                                })
                            idx_counter += 1

                    # 5) Flatten out for sub-osc recursion
                    base_results_list = []
                    for val in formatted_base_results.values():
                        base_results_list.extend(val["results"])

                    # 6) If sub-oscillation is requested, build deeper levels
                    if sub_oscillation_search and sub_oscillation_level > 0 and base_results_list:
                        perform_sub_oscillation_search(
                            base_results_list,
                            initial_gem,
                            sub_oscillation_level,
                            include_torah,
                            include_bible,
                            include_quran,
                            include_hindu,
                            include_tripitaka
                        )

                    # 7) Now we prune (filter) the results according to the user’s search criteria
                    #    For each "book" block, we filter the "results" array
                    pruned_base_results = {}
                    for key, block in formatted_base_results.items():
                        original_list = block["results"]  # each is {book, result, subresults}
                        filtered_list = prune_tree_by_search_criteria(
                            original_list,
                            search_type,
                            search_term,
                            gematria_sum_search,
                            search_mode
                        )
                        # Keep only if we got something back
                        if filtered_list:
                            pruned_base_results[key] = {
                                "book": block["book"],
                                "results": filtered_list
                            }

                    # If there’s nothing left after filtering, skip adding
                    if not pruned_base_results:
                        continue

                    # 8) Build the final item for this date+name, with pruned results
                    final_item = {
                        "date": date_str,
                        "name": name,
                        "search_path_basic": search_path_basic,
                        "basic_gematria": initial_gem,
                        "basic_results": pruned_base_results
                    }

                    # 9) If translation is requested, only translate the pruned results
                    if translate_results:
                        # Recursively translate only the items in pruned_base_results
                        for block_key, block_val in final_item["basic_results"].items():
                            for entry in block_val["results"]:
                                txt = entry["result"].get("result_text", "")
                                src_lang = entry["result"].get("source_language", "auto")
                                if txt:
                                    trans = translation_utils.get_translation(txt, "en", src_lang)[0]
                                    entry["result"]["translated_text"] = trans
                                # And go deeper into subresults
                                if entry["subresults"]:
                                    translate_subresults_recursive(entry["subresults"])

                    # 10) Add this final, fully filtered (and possibly translated) item
                    all_filtered_results.append(final_item)

            # Return the fully processed set of results
            return all_filtered_results

        def remove_duplicates(dict_list):
            """
            If you need to remove duplicates at any stage, you can call this,
            but it's optional if your data doesn't require deduping.
            """
            seen = set()
            unique_list = []
            for item in dict_list:
                item_id = repr(item)
                if item_id not in seen:
                    seen.add(item_id)
                    unique_list.append(item)
            return unique_list


        perform_search_btn_els.click(
            perform_date_range_els_search,
            inputs=[
                start_date_els,
                end_date_els,
                names_input_els,
                search_type_els,
                search_term_els,
                gematria_sum_search_els,
                search_mode_els,
                include_torah_chk_els,
                include_bible_chk_els,
                include_quran_chk_els,
                include_hindu_chk_els,
                include_tripitaka_chk_els,
                translate_results_chk_els,
                sub_oscillation_search_chk_els,
                sub_oscillation_level_els
            ],
            outputs=[filtered_results_output_els]
        )


    with gr.Tab("ELS Journal Gematria-sum Synchronicity search"):
        with gr.Row():
            start_date_comp = Calendar(type="datetime", label="Start Date")
            end_date_comp = Calendar(type="datetime", label="End Date")
        with gr.Row():
            names_input_comp = gr.Textbox(label="Names (one per line)", lines=5)
        with gr.Row():
            sub_oscillation_level_comp = gr.Number(label="Sub-Oscillation Level", precision=0, value=0)
        with gr.Row():
            include_torah_chk_comp = gr.Checkbox(label="Include Torah", value=True)
            include_bible_chk_comp = gr.Checkbox(label="Include Bible", value=True)
            include_quran_chk_comp = gr.Checkbox(label="Include Quran", value=True)
            include_hindu_chk_comp = gr.Checkbox(label="Include Rigveda", value=False)
            include_tripitaka_chk_comp = gr.Checkbox(label="Include Tripitaka", value=False)
        with gr.Row():
            translate_results_chk_comp = gr.Checkbox(label="Translate Results to English", value=False)
        with gr.Row():
            common_gematria_results = gr.JSON(label="Common Gematria Results")
        with gr.Row():
            perform_search_btn_comp = gr.Button("Perform Search")



        def perform_comprehensive_els_search(
            start_date,
            end_date,
            names_input,
            sub_oscillation_level,
            include_torah,
            include_bible,
            include_quran,
            include_hindu,
            include_tripitaka,
            translate_results
        ):
            names = [n.strip() for n in names_input.split("\n") if n.strip()]
            date_gematrias = perform_gematria_calculation_for_date_range(start_date, end_date)

            all_results_by_date = {}

            for date_str, date_data in date_gematrias.items():
                all_results_by_date[date_str] = {}
                for name in names:
                    name_gem = calculate_gematria_sum(name, "")
                    initial_gem = date_data["date_gematria"] + name_gem

                    base_results_list = []
                    base_results = perform_els_search_for_gematria_sum(
                        gematria_sum=initial_gem,
                        include_torah=include_torah,
                        include_bible=include_bible,
                        include_quran=include_quran,
                        include_hindu=include_hindu,
                        include_tripitaka=include_tripitaka
                    )
                    for book_name, res_list in base_results.items():
                        for one_res in res_list:
                            if not isinstance(one_res, dict) or "result_sum" not in one_res:
                                print(f"Warning: Invalid result format for {name} on {date_str}: {one_res}")
                                continue

                            base_results_list.append({
                                        "book": book_name,
                                        "result": one_res,
                                        "subresults": []
                                    })


                    if sub_oscillation_level > 0 and base_results_list:
                        perform_sub_oscillation_search(
                            base_results_list,
                            initial_gem,
                            sub_oscillation_level,
                            include_torah,
                            include_bible,
                            include_quran,
                            include_hindu,
                            include_tripitaka
                        )

                    all_results_by_date[date_str][name] = base_results_list


            common_results = {}
            for date_str, name_results in all_results_by_date.items():
                common_results[date_str] = {}
                sums_by_name = {}
                all_sums_count = Counter()
                for name, results in name_results.items():
                    sums_by_name[name] = set()
                    count_sums_recursive(results, all_sums_count)
                    extract_sums_recursive(results, sums_by_name[name])

                ordered_common_sums = [item for item, count in all_sums_count.most_common() if all(item in sums_by_name[n] for n in sums_by_name)]

                for common_sum in ordered_common_sums:
                    common_results[date_str][str(common_sum)] = {}
                    for name, results in name_results.items():
                        matching_items = []
                        for item in results:
                            if find_results_by_sum([item], common_sum):
                                item_copy = copy.deepcopy(item)

                                if translate_results:
                                    translate_recursive(item_copy)


                                item_copy["subresults"] = find_results_by_sum(item.get("subresults", []), common_sum)

                                if translate_results:
                                    for sub_item in item_copy["subresults"]:
                                        translate_recursive(sub_item)

                                matching_items.append(item_copy)

                        if matching_items:
                            common_results[date_str][str(common_sum)][name] = matching_items

            return common_results

        def find_results_by_sum(results, target_sum):
            matches = []
            if isinstance(results, list):
                for item in results:
                    matches.extend(find_results_by_sum(item, target_sum))
            elif isinstance(results, dict):
                if "result" in results and isinstance(results["result"], dict) and "result_sum" in results["result"]:
                    try:
                        result_sum = int(results["result"]["result_sum"])
                        if result_sum == target_sum:
                            matches.append(results)
                    except (TypeError, ValueError) as e:
                        print(f"Error comparing result_sum: {e}, Data: {results}")
                if "subresults" in results:
                    matches.extend(find_results_by_sum(results["subresults"], target_sum))

            return matches


        def extract_sums_recursive(data, sums_set):
            if isinstance(data, list):
                for item in data:
                    extract_sums_recursive(item, sums_set)
            elif isinstance(data, dict):
                if "result" in data and isinstance(data["result"], dict) and "result_sum" in data["result"]:
                    try:
                        result_sum = int(data["result"]["result_sum"])
                        sums_set.add(result_sum)
                    except (TypeError, ValueError) as e:
                        print(f"Error extracting result_sum: {e}, Data: {data}")

                if "subresults" in data:
                    extract_sums_recursive(data["subresults"], sums_set)


        def count_sums_recursive(data, sums_counter):
            if isinstance(data, list):
                for item in data:
                    count_sums_recursive(item, sums_counter)
            elif isinstance(data, dict):
                if "result" in data and isinstance(data["result"], dict) and "result_sum" in data["result"]:
                    try:
                        result_sum = int(data["result"]["result_sum"])
                        sums_counter[result_sum] += 1
                    except (TypeError, ValueError) as e:
                        print(f"Error counting result_sum: {e}, Data: {data}")
                if "subresults" in data:
                    count_sums_recursive(data["subresults"], sums_counter)



        def translate_recursive(item):
            if "result" in item and isinstance(item["result"], dict) and "result_text" in item["result"]:
                text = item["result"].get("result_text", "")
                source_lang = item["result"].get("source_language", "auto")
                if text:
                    translated_text = translation_utils.get_translation(text, "en", source_lang)[0]
                    item["result"]["translated_text"] = translated_text

            if "subresults" in item and isinstance(item["subresults"], list):
                for sub in item["subresults"]:
                    translate_recursive(sub)




        perform_search_btn_comp.click(
            perform_comprehensive_els_search,
            inputs=[
                start_date_comp,
                end_date_comp,
                names_input_comp,
                sub_oscillation_level_comp,
                include_torah_chk_comp,
                include_bible_chk_comp,
                include_quran_chk_comp,
                include_hindu_chk_comp,
                include_tripitaka_chk_comp,
                translate_results_chk_comp
            ],
            outputs=[common_gematria_results]
        )

    # --- Event Handlers ---

    search_type.change(
        fn=update_search_components,
        inputs=[search_type],
        outputs=[search_term, gematria_sum_search]
    )

    search_db_btn.click(
        fn=search_cache_database,
        inputs=[search_type, search_term, gematria_sum_search, main_book_filter, rounds_filter, search_mode],
        outputs=cache_search_results
    )


    def update_rounds_choices():
        return gr.update(choices=extract_rounds_combinations()) # gr.update, nicht gr.Dropdown.update

    app.load(fn=update_rounds_choices, inputs=None, outputs=rounds_filter)

    main_book_filter.change(
        fn=update_rounds_choices,
        inputs=None, # No input needed here
        outputs=rounds_filter
    )

    # rest of the handlers
    def update_date_words(selected_date, date_language_input, use_day, use_month, use_year):
        if selected_date is None:
            return ""

        if not use_year and not use_month and not use_day:
            return translate_date_to_words(selected_date, date_language_input)

        year = selected_date.year if use_year else None
        month = selected_date.month if use_month else None
        day = selected_date.day if use_day else None

        if year is not None and month is not None and day is not None:
            date_obj = selected_date
        elif year is not None and month is not None:
            date_obj = str(f"{year}-{month}")
        elif year is not None:
            date_obj = str(f"{year}")
        else:  # Return empty string if no date components are selected
            return ""

        date_in_words = date_to_words(date_obj)

        translator = GoogleTranslator(source='auto', target=date_language_input)
        translated_date_words = translator.translate(date_in_words)
        return custom_normalize(translated_date_words)

    def update_journal_sum(gematria_text, date_words_output):
        sum_value = calculate_gematria_sum(gematria_text, date_words_output)
        return sum_value, sum_value, sum_value

    def update_rounds_combination(round_x, round_y):
        return f"{int(round_x)},{int(round_y)}"

    def update_step_half(float_step):
        new_step = math.ceil(float_step / 2)
        return new_step, float_step / 2

    def update_step_double(float_step):
        new_step = math.ceil(float_step * 2)
        return new_step, float_step * 2

    def find_closest_phrase(target_phrase, phrases):
        best_match = None
        best_score = 0

        logging.debug(f"Target phrase for similarity search: {target_phrase}")

        for phrase, _, _, _, _, _ in phrases:
            word_length_diff = abs(len(target_phrase.split()) - len(phrase.split()))
            similarity_score = fuzz.ratio(target_phrase, phrase)
            combined_score = similarity_score - word_length_diff

            logging.debug(f"Comparing with phrase: {phrase}")
            logging.debug(
                f"Word Length Difference: {word_length_diff}, Similarity Score: {similarity_score}, Combined Score: {combined_score}")

            if combined_score > best_score:
                best_score = combined_score
                best_match = phrase

        logging.debug(f"Closest phrase found: {best_match} with score: {best_score}")
        return best_match

    def perform_search(step, rounds_combination, tlang, strip_spaces, strip_in_braces, strip_diacritics_chk,
                       include_torah, include_bible, include_quran, include_hindu, include_tripitaka, gematria_text,
                       date_words_output, selected_date):
        els_results = perform_els_search(step, rounds_combination, tlang, strip_spaces, strip_in_braces,
                                         strip_diacritics_chk, include_torah, include_bible, include_quran,
                                         include_hindu,
                                         include_tripitaka)

        most_frequent_phrases = {}
        combined_and_sorted_results = []

        for book_name, book_results in els_results.items():
            if book_results:
                most_frequent_phrases[book_name] = ""

                for result in book_results:
                    try:
                        gematria_sum = calculate_gematria(result['result_text'])
                        max_words = len(result['result_text'].split())
                        matching_phrases = search_gematria_in_db(gematria_sum, max_words)
                        max_words_limit = 20
                        while not matching_phrases and max_words < max_words_limit:
                            max_words += 1
                            matching_phrases = search_gematria_in_db(gematria_sum, max_words)

                        if matching_phrases:
                            most_frequent_phrase = get_most_frequent_phrase(matching_phrases)
                            most_frequent_phrases[book_name] = most_frequent_phrase
                        else:
                            # closest_phrase = find_closest_phrase(result['result_text'],
                            #                                      search_gematria_in_db(gematria_sum, max_words_limit)) # Removed fuzzywuzzy
                            most_frequent_phrases[
                                book_name] = "" # closest_phrase or ""

                        result['Most Frequent Phrase'] = most_frequent_phrases[book_name]
                        if 'book' in result:
                            if isinstance(result['book'], int):
                                result['book'] = f"{book_name} {result['book']}."
                        combined_and_sorted_results.append(result)

                    except KeyError as e:
                        print(f"DEBUG: KeyError - Key '{e.args[0]}' not found in result. Skipping this result.")
                        continue

        selected_language_long = tlang
        tlang_short = LANGUAGES_SUPPORTED.get(selected_language_long)
        if tlang_short is None:
            tlang_short = "en"
            logger.warning(f"Unsupported language selected: {selected_language_long}. Defaulting to English (en).")

        phrases_to_translate = []
        phrases_source_langs = []
        results_to_translate = []
        results_source_langs = []
        for result in combined_and_sorted_results:
            phrases_to_translate.append(result.get('Most Frequent Phrase', ''))
            phrases_source_langs.append("he")
            results_to_translate.append(result.get('result_text', ''))
            results_source_langs.append(result.get("source_language", "auto"))

        translated_phrases = translation_utils.batch_translate(phrases_to_translate, tlang_short,
                                                               phrases_source_langs)
        translated_result_texts = translation_utils.batch_translate(results_to_translate, tlang_short,
                                                                    results_source_langs)

        for i, result in enumerate(combined_and_sorted_results):
            result['translated_text'] = translated_result_texts.get(results_to_translate[i], None)
            result['Translated Most Frequent Phrase'] = translated_phrases.get(phrases_to_translate[i], None)

        updated_els_results = add_24h_projection(els_results)
        updated_els_results = add_monthly_projection(updated_els_results, selected_date)
        updated_els_results = add_yearly_projection(updated_els_results, selected_date)

        combined_and_sorted_results = []
        for book_results in updated_els_results.values():
            combined_and_sorted_results.extend(book_results)
        combined_and_sorted_results = sort_results(combined_and_sorted_results)

        df = pd.DataFrame(combined_and_sorted_results)
        df.index = range(1, len(df) + 1)
        df.reset_index(inplace=True)
        df.rename(columns={'index': 'Result Number'}, inplace=True)

        for i, result in enumerate(combined_and_sorted_results):
            result['Result Number'] = i + 1

        search_config = {
            "step": step,
            "rounds_combination": rounds_combination,
            "target_language": tlang,
            "strip_spaces": strip_spaces,
            "strip_in_braces": strip_in_braces,
            "strip_diacritics": strip_diacritics_chk,
            "include_torah": include_torah,
            "include_bible": include_bible,
            "include_quran": include_quran,
            "include_hindu": include_hindu,
            "include_tripitaka": include_tripitaka,
            "gematria_text": gematria_text,
            "date_words": date_words_output
        }

        output_data = {
            "search_configuration": search_config,
            "results": combined_and_sorted_results
        }

        json_data = output_data

        combined_most_frequent = "\n".join(
            f"{book}: {phrase}" for book, phrase in most_frequent_phrases.items())
        return df, combined_most_frequent, json_data

    # --- Event Triggers ---
    round_x.change(update_rounds_combination, inputs=[round_x, round_y], outputs=rounds_combination)
    round_y.change(update_rounds_combination, inputs=[round_x, round_y], outputs=rounds_combination)

    selected_date.change(update_date_words, inputs=[selected_date, date_language_input, use_day, use_month, use_year],
                         outputs=[date_words_output])
    date_language_input.change(update_date_words,
                               inputs=[selected_date, date_language_input, use_day, use_month, use_year],
                               outputs=[date_words_output])

    gematria_text.change(update_journal_sum, inputs=[gematria_text, date_words_output],
                         outputs=[gematria_result, step, float_step])
    date_words_output.change(update_journal_sum, inputs=[gematria_text, date_words_output],
                             outputs=[gematria_result, step, float_step])

    half_step_btn.click(update_step_half, inputs=[float_step], outputs=[step, float_step])
    double_step_btn.click(update_step_double, inputs=[float_step], outputs=[step, float_step])

    translate_btn.click(
        perform_search,
        inputs=[step, rounds_combination, tlang, strip_spaces, strip_in_braces, strip_diacritics_chk, include_torah_chk,
                include_bible_chk, include_quran_chk, include_hindu_chk, include_tripitaka_chk, gematria_text,
                date_words_output, selected_date],
        outputs=[markdown_output, most_frequent_phrase_output, json_output]
    )

    app.load(
        update_date_words,
        inputs=[selected_date, date_language_input, use_day, use_month, use_year],
        outputs=[date_words_output]
    )

    use_day.change(
        update_date_words,
        inputs=[selected_date, date_language_input, use_day, use_month, use_year],
        outputs=[date_words_output]
    )
    use_month.change(
        update_date_words,
        inputs=[selected_date, date_language_input, use_day, use_month, use_year],
        outputs=[date_words_output]
    )
    use_year.change(
        update_date_words,
        inputs=[selected_date, date_language_input, use_day, use_month, use_year],
        outputs=[date_words_output]
    )

    def checkbox_behavior(use_day_value, use_month_value):
        if use_day_value:
            return True, True

        return use_month_value, True

    use_day.change(checkbox_behavior, inputs=[use_day, use_month], outputs=[use_month, use_year])
    use_month.change(checkbox_behavior, inputs=[use_day, use_month], outputs=[use_month, use_year])

if __name__ == "__main__":
    app.launch(share=False)