import calendar
import subprocess
from datetime import datetime, timedelta

import gi
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label

import modules.icons as icons

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, Gio


class Calendar(Gtk.Box):
    def __init__(self, view_mode="month"):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8, name="calendar")
        self.view_mode = view_mode
        self.first_weekday = 0  # Default: Monday, will be updated async

        self.set_halign(Gtk.Align.CENTER)
        self.set_hexpand(False)

        self.current_day_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)

        if self.view_mode == "month":
            self.current_shown_date = self.current_day_date.replace(day=1)
            self.current_year = self.current_shown_date.year
            self.current_month = self.current_shown_date.month
            self.current_day = self.current_day_date.day # Solo para resaltar en create_month_view
            self.previous_key = (self.current_year, self.current_month)
        elif self.view_mode == "week":
            # current_shown_date es el primer día (según locale) de la semana actual
            days_to_subtract = (self.current_day_date.weekday() - self.first_weekday + 7) % 7
            self.current_shown_date = self.current_day_date - timedelta(days=days_to_subtract)
            self.current_year = self.current_shown_date.year # Para el header
            self.current_month = self.current_shown_date.month # Para el header
            iso_year, iso_week, _ = self.current_shown_date.isocalendar()
            self.previous_key = (iso_year, iso_week)
            self.set_halign(Gtk.Align.FILL)
            self.set_hexpand(True)
            self.set_valign(Gtk.Align.CENTER)
            self.set_vexpand(False)
        
        self.cache_threshold = 3 # Umbral para mantener vistas en caché

        self.month_views = {} # Reutilizado para vistas de semana también

        self.prev_button = Gtk.Button( # Nombre genérico del botón
            name="prev-month-button", 
            child=Label(name="month-button-label", markup=icons.chevron_left) # CSS puede ser genérico
        )
        self.prev_button.connect("clicked", self.on_prev_clicked)

        self.month_label = Gtk.Label(name="month-label") # El nombre es histórico, pero muestra mes/año

        self.next_button = Gtk.Button( # Nombre genérico del botón
            name="next-month-button",
            child=Label(name="month-button-label", markup=icons.chevron_right) # CSS puede ser genérico
        )
        self.next_button.connect("clicked", self.on_next_clicked)

        self.header = CenterBox(
            spacing=4,
            name="header",
            start_children=[self.prev_button],
            center_children=[self.month_label],
            end_children=[self.next_button],
        )

        self.add(self.header)

        self.weekday_row = Gtk.Box(spacing=4, name="weekday-row")
        self.pack_start(self.weekday_row, False, False, 0)

        self.stack = Gtk.Stack(name="calendar-stack")
        self.stack.set_transition_duration(250)
        self.pack_start(self.stack, True, True, 0)

        self.update_header() # Llamar antes de update_calendar para que el primer header sea correcto
        self.update_calendar()
        self.setup_periodic_update()
        self.setup_dbus_listeners()

        # Initialize locale settings asynchronously
        GLib.Thread.new("calendar-locale", self._init_locale_settings_thread, None)

    def _init_locale_settings_thread(self, user_data):
        """Background thread to initialize locale settings without blocking UI."""
        try:
            origin_date_str = subprocess.check_output(["locale", "week-1stday"], text=True).strip()
            first_weekday_val = int(subprocess.check_output(["locale", "first_weekday"], text=True).strip())
            
            origin_date = datetime.fromisoformat(origin_date_str)
            # Esta lógica calcula el día de la semana (0-6, Lunes=0) que es considerado el primero
            # según la configuración regional combinada de week-1stday y first_weekday.
            date_of_first_day_of_week_config = origin_date + timedelta(days=first_weekday_val - 1)
            new_first_weekday = date_of_first_day_of_week_config.weekday() # Lunes=0, ..., Domingo=6
            
            # Update the first_weekday on main thread and refresh calendar if needed
            GLib.idle_add(self._update_first_weekday, new_first_weekday)
        except Exception as e:
            print(f"Error getting locale first weekday: {e}")
            # Keep default value (0 = Monday)
    
    def _update_first_weekday(self, new_first_weekday):
        """Update first weekday setting and refresh calendar if changed."""
        if self.first_weekday != new_first_weekday:
            self.first_weekday = new_first_weekday
            # Clear cache and refresh calendar with new locale settings
            self.month_views.clear()
            # Remove all current stack children to force regeneration
            for child in self.stack.get_children():
                self.stack.remove(child)
            # Update header (which includes weekday labels) and calendar
            self.update_header()
            self.update_calendar()
        return False  # Don't repeat this idle callback

    def setup_periodic_update(self):
        # Check for date changes every second
        GLib.timeout_add(1000, self.check_date_change)

    def setup_dbus_listeners(self):
        # Listen for system suspend/resume events
        bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
        bus.signal_subscribe(
            None,  # sender
            'org.freedesktop.login1.Manager',  # interface
            'PrepareForSleep',  # signal
            '/org/freedesktop/login1',  # path
            None,  # arg0
            Gio.DBusSignalFlags.NONE,
            self.on_suspend_resume,  # callback
            None  # user_data
        )

    def check_date_change(self):
        now = datetime.now()
        current_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
        if current_date != self.current_day_date:
            self.on_midnight()
        return True  # Continue the timer

    def on_suspend_resume(self, connection, sender_name, object_path, interface_name, signal_name, parameters, user_data):
        # Check date when resuming from suspend
        self.check_date_change()

    def on_midnight(self):
        now = datetime.now()
        self.current_day_date = now.replace(hour=0, minute=0, second=0, microsecond=0)

        key_to_remove_for_today_highlight = None
        if self.view_mode == "month":
            # Actualizar la fecha base para la vista de mes si es necesario (aunque usualmente no cambia a medianoche)
            self.current_shown_date = self.current_day_date.replace(day=1)
            self.current_year = self.current_shown_date.year
            self.current_month = self.current_shown_date.month
            self.current_day = self.current_day_date.day # Actualizar el día actual
            key_to_remove_for_today_highlight = (self.current_year, self.current_month)
        elif self.view_mode == "week":
            days_to_subtract = (self.current_day_date.weekday() - self.first_weekday + 7) % 7
            self.current_shown_date = self.current_day_date - timedelta(days=days_to_subtract)
            self.current_year = self.current_shown_date.year # Para el header
            self.current_month = self.current_shown_date.month # Para el header
            iso_year, iso_week, _ = self.current_shown_date.isocalendar()
            key_to_remove_for_today_highlight = (iso_year, iso_week)

        # Eliminar la vista actual de la caché para forzar la regeneración con el nuevo "hoy" resaltado
        if key_to_remove_for_today_highlight and key_to_remove_for_today_highlight in self.month_views:
            widget = self.month_views.pop(key_to_remove_for_today_highlight)
            self.stack.remove(widget)
            # Si la vista eliminada era la actual, previous_key podría quedar desactualizado
            # pero update_calendar lo corregirá al establecer la nueva vista.

        self.update_calendar() # Esto regenerará la vista si fue eliminada y actualizará el resaltado
        return False # Importante para que el timeout no se repita automáticamente

    def update_header(self):
        # self.current_shown_date es el primer día del mes (modo mes) o el primer día de la semana (modo semana)
        # El encabezado siempre muestra el mes y año de self.current_shown_date
        self.month_label.set_text(
            self.current_shown_date.strftime("%B %Y").capitalize()
        )

        for child in self.weekday_row.get_children():
            self.weekday_row.remove(child)
        
        day_initials = self.get_weekday_initials()
        for day_initial in day_initials:
            label = Gtk.Label(label=day_initial.upper(), name="weekday-label")
            self.weekday_row.pack_start(label, True, True, 0)
        self.weekday_row.show_all()

    def update_calendar(self):
        new_key = None
        child_name = "" # Renombrado de child_name_prefix
        view_widget = None

        if self.view_mode == "month":
            new_key = (self.current_year, self.current_month)
            child_name = f"{self.current_year}_{self.current_month}"
            if new_key not in self.month_views:
                view_widget = self.create_month_view(self.current_year, self.current_month)
        elif self.view_mode == "week":
            iso_year, iso_week, _ = self.current_shown_date.isocalendar()
            new_key = (iso_year, iso_week)
            child_name = f"{iso_year}_w{iso_week}"
            if new_key not in self.month_views:
                # Pasar self.current_shown_date directamente a create_week_view
                view_widget = self.create_week_view(self.current_shown_date)
        
        if new_key is None: return

        if new_key > self.previous_key:
            self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
        elif new_key < self.previous_key:
            self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
        # else: no transition if key is the same (e.g. on_midnight for same month/week)

        self.previous_key = new_key

        if view_widget: # Si se creó una nueva vista
            self.month_views[new_key] = view_widget
            self.stack.add_titled(view_widget, child_name, child_name)
        
        self.stack.set_visible_child_name(child_name)
        # El encabezado se actualiza ANTES de llamar a update_calendar en __init__ y on_clicked,
        # y también en on_midnight si es necesario.
        # Pero si la vista cambia (ej. de Enero a Febrero), el encabezado debe reflejarlo.
        self.update_header() # Asegurar que el header está sincronizado con la vista actual
        self.stack.show_all()

        self.prune_cache()

    def prune_cache(self):
        def get_key_index(key_tuple):
            year, num = key_tuple # num es month o week_number
            if self.view_mode == "month": # Asumiendo que la clave es (año, mes)
                return year * 12 + (num - 1)
            else: # Asumiendo que la clave es (año_iso, semana_iso)
                return year * 53 + num # Usar 53 para cubrir años con 53 semanas ISO

        current_index = get_key_index(self.previous_key) # previous_key es la clave de la vista actual
        keys_to_remove = []
        for key_iter in self.month_views:
            if abs(get_key_index(key_iter) - current_index) > self.cache_threshold:
                keys_to_remove.append(key_iter)
        for key_to_remove in keys_to_remove:
            widget = self.month_views.pop(key_to_remove)
            self.stack.remove(widget)

    def create_month_view(self, year, month):
        grid = Gtk.Grid(column_homogeneous=True, row_homogeneous=False, name="calendar-grid")
        cal = calendar.Calendar(firstweekday=self.first_weekday)
        month_days = cal.monthdayscalendar(year, month)

        while len(month_days) < 6: # Asegurar 6 filas para consistencia visual
            month_days.append([0] * 7) # [0] representa un día vacío

        for row, week in enumerate(month_days):
            for col, day_num in enumerate(week):
                day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box")
                top_spacer = Gtk.Box(hexpand=True, vexpand=True)
                middle_box = Gtk.Box(hexpand=True, vexpand=True)
                bottom_spacer = Gtk.Box(hexpand=True, vexpand=True)

                if day_num == 0:
                    label = Label(name="day-empty", markup=icons.dot)
                else:
                    label = Gtk.Label(label=str(day_num), name="day-label")
                    day_date_obj = datetime(year, month, day_num)
                    if day_date_obj == self.current_day_date:
                        label.get_style_context().add_class("current-day")
                
                middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
                middle_box.pack_start(label, False, False, 0)
                middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)

                day_box.pack_start(top_spacer, True, True, 0)
                day_box.pack_start(middle_box, True, True, 0)
                day_box.pack_start(bottom_spacer, True, True, 0)
                grid.attach(day_box, col, row, 1, 1)
        grid.show_all()
        return grid

    def create_week_view(self, first_day_of_week_to_display):
        grid = Gtk.Grid(column_homogeneous=True, row_homogeneous=False, name="calendar-grid-week-view") # Podría tener estilo diferente
        
        # El mes de referencia para atenuar es el mes de first_day_of_week_to_display
        # que es self.current_shown_date, y su mes es self.current_month (actualizado en nav)
        reference_month_for_dimming = first_day_of_week_to_display.month

        for col in range(7):
            current_day_in_loop = first_day_of_week_to_display + timedelta(days=col)
            
            day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box") # Reusar estilo de day-box
            top_spacer = Gtk.Box(hexpand=True, vexpand=True)
            middle_box = Gtk.Box(hexpand=True, vexpand=True)
            bottom_spacer = Gtk.Box(hexpand=True, vexpand=True)

            label = Gtk.Label(label=str(current_day_in_loop.day), name="day-label")

            if current_day_in_loop == self.current_day_date:
                label.get_style_context().add_class("current-day")
            
            if current_day_in_loop.month != reference_month_for_dimming:
                 label.get_style_context().add_class("dim-label") # Necesita CSS: .dim-label { opacity: 0.5; } o similar

            middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
            middle_box.pack_start(label, False, False, 0)
            middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)

            day_box.pack_start(top_spacer, True, True, 0)
            day_box.pack_start(middle_box, True, True, 0)
            day_box.pack_start(bottom_spacer, True, True, 0)
            
            grid.attach(day_box, col, 0, 1, 1) # Todos los días en la fila 0
        
        # Para mantener una altura similar a la vista mensual, se podrían añadir filas vacías.
        # Esto es opcional y depende del diseño deseado.
        # for r_idx in range(1, 6): # Añadir 5 filas vacías
        #     empty_row_placeholder = Gtk.Box(name="day-empty-placeholder", hexpand=True, vexpand=True, height_request=20) # Ajustar altura
        #     grid.attach(empty_row_placeholder, 0, r_idx, 7, 1) # Abarca las 7 columnas

        grid.show_all()
        return grid

    def get_weekday_initials(self):
        # Genera las iniciales de los días de la semana comenzando por self.first_weekday
        # datetime(2024, 1, 1) es Lunes. Su weekday() es 0.
        # Si self.first_weekday es 0 (Lunes), queremos que el primer día sea Lunes.
        #   i=0: datetime(2024, 1, 1 + 0) -> Lunes
        # Si self.first_weekday es 6 (Domingo), queremos que el primer día sea Domingo.
        #   i=0: datetime(2024, 1, 1 + 6) -> Domingo
        # Esta lógica es correcta.
        return [(datetime(2024, 1, 1) + timedelta(days=(self.first_weekday + i) % 7)).strftime("%a")[:1] for i in range(7)]


    def on_prev_clicked(self, widget):
        if self.view_mode == "month":
            current_month_val = self.current_shown_date.month
            current_year_val = self.current_shown_date.year
            if current_month_val == 1:
                self.current_shown_date = self.current_shown_date.replace(year=current_year_val - 1, month=12)
            else:
                self.current_shown_date = self.current_shown_date.replace(month=current_month_val - 1)
            self.current_year = self.current_shown_date.year
            self.current_month = self.current_shown_date.month
        elif self.view_mode == "week":
            self.current_shown_date -= timedelta(days=7)
            self.current_year = self.current_shown_date.year # Actualizar para el header
            self.current_month = self.current_shown_date.month # Actualizar para el header y dimming
        
        # self.update_header() # Se llama dentro de update_calendar
        self.update_calendar()

    def on_next_clicked(self, widget):
        if self.view_mode == "month":
            current_month_val = self.current_shown_date.month
            current_year_val = self.current_shown_date.year
            if current_month_val == 12:
                self.current_shown_date = self.current_shown_date.replace(year=current_year_val + 1, month=1)
            else:
                self.current_shown_date = self.current_shown_date.replace(month=current_month_val + 1)
            self.current_year = self.current_shown_date.year
            self.current_month = self.current_shown_date.month
        elif self.view_mode == "week":
            self.current_shown_date += timedelta(days=7)
            self.current_year = self.current_shown_date.year # Actualizar para el header
            self.current_month = self.current_shown_date.month # Actualizar para el header y dimming

        # self.update_header() # Se llama dentro de update_calendar
        self.update_calendar()
