"""
bal.gui.qt.calendar
===================

iCalendar (.ics) generation and "open with default calendar app" helper.

When a will is built, the plugin can create a calendar event reminding the user
to "check in" before the locktime expires.  This module turns the event data
into an RFC-5545 .ics file and opens it with the OS default application.
"""

from .common import *
from .common import _, _logger  # underscore names are not re-exported by "import *"

class BalCalendar:
    @staticmethod
    def write_temp_ics(content):
        fd, path = tempfile.mkstemp(prefix="event_", suffix=".ics")
        with os.fdopen(fd, "wb") as f:
            f.write(content.encode("utf-8"))
        return path

    @staticmethod
    def open_with_default_app(calendar_app, path):
        _logger.debug("opening calendar app")
        try:
            subprocess.check_call([calendar_app, path])
            return True
        except Exception as e:
            _logger.error(f"starting calendar app {e}")
            return False


    @staticmethod
    def format_time(time):
        return time.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        #return time.astimezone(timezone.utc).strftime("%Y%m%d")

    @staticmethod
    def compute_event_timestamp(will_settings_locktime, min_valid_tx_locktime):
        """Return the UNIX timestamp the calendar event should start at.

        The reminder must fire at the EARLIEST moment the inheritance could be
        executed, i.e. the lower value between:

            * ``will_settings_locktime`` - the deadline configured in the will
              settings, and
            * ``min_valid_tx_locktime``  - the locktime of the valid will
              transaction that expires first (``None`` when there is no valid
              transaction yet).

        When there is no valid transaction the will-settings locktime is used.
        """
        ws = int(will_settings_locktime)
        if min_valid_tx_locktime is None:
            return ws
        return min(ws, int(min_valid_tx_locktime))

    @staticmethod
    def build_alarms(num_alarms, alarm_start, alarm_end):
        """Build RFC-5545 ``VALARM`` blocks spread before the deadline.

        The window between ``alarm_start`` (the later of "now" and the
        check-alive threshold) and ``alarm_end`` (the deadline / event time) is
        divided into ``num_alarms + 1`` equal slices; an alarm is placed at each
        of the first ``num_alarms`` division points.  Each alarm uses a
        ``TRIGGER;RELATED=END`` negative duration so it fires *before* the event
        end, which every major online calendar (Google, Apple, Outlook)
        understands.

        Args:
            num_alarms: how many reminders to create (0 -> no alarms).
            alarm_start: ``datetime`` when reminders should start being useful.
            alarm_end: ``datetime`` of the deadline (the event end).

        Returns:
            A list of .ics lines (``BEGIN:VALARM`` ... ``END:VALARM`` blocks).
        """
        if num_alarms < 1:
            return []
        total_seconds = max(1, int((alarm_end - alarm_start).total_seconds()))
        # Divide the total time by (num_alarms + 1) and place alarms on the
        # first num_alarms division points (measured back from the deadline).
        interval = total_seconds // (num_alarms + 1)
        lines = []
        for i in range(1, num_alarms + 1):
            # Offset measured from the END (deadline): the closest alarm to the
            # deadline is the smallest offset.
            offset_seconds = interval * (num_alarms + 1 - i)
            trigger = BalCalendar._duration_trigger(offset_seconds)
            lines.extend([
                "BEGIN:VALARM",
                f"TRIGGER;RELATED=END:-{trigger}",
                "ACTION:DISPLAY",
                "DESCRIPTION:Bitcoin After Life reminder",
                "END:VALARM",
            ])
        return lines

    @staticmethod
    def _duration_trigger(offset_seconds):
        """Format ``offset_seconds`` as an RFC-5545 duration (e.g. ``P3D``).

        Uses the coarsest sensible unit so the value stays readable and within
        what calendar clients expect: days (``P<n>D``), hours (``PT<n>H``),
        minutes (``PT<n>M``) or seconds (``PT<n>S``).
        """
        if offset_seconds <= 0:
            return "PT0S"
        if offset_seconds >= 86400:
            return f"P{offset_seconds // 86400}D"
        if offset_seconds >= 3600:
            return f"PT{offset_seconds // 3600}H"
        if offset_seconds >= 60:
            return f"PT{offset_seconds // 60}M"
        return f"PT{offset_seconds}S"

    @staticmethod
    def ical_escape(text: str) -> str:
        # Escape special characters per RFC 5545: backslash, ";", ",", newlines.
        text = text.encode("utf-8")
        text = (
            text.replace(b"\\", b"\\\\")
            .replace(b";", b"\\;")
            .replace(b",", b"\\,")
        )
        out =""
        temp=text.split(b"\r\n")
        for s in temp:
            encoded= s
            cut =0
            while len(encoded) >75:
                cut+=5
                encoded=f"{s[:len(s)-cut]}"
                if encoded[-1]==b"\\" and encoded[-2]!=b"\\\\":
                    cut += 1
                encoded=f"{s[:len(s)-cut]}"
                encoded=f"{encoded}...\r\n".encode("utf-8")
            if cut>0:
                out+=str(f"{s[:len(s)-cut].decode()}...\r\n")
            else:
                out+=str(f"{s.decode()}\r\n")

        return out[:-2]

    @staticmethod
    def fold_ical_line(line: str, limit: int = 75) -> str:
        # Return the line folded per RFC 5545: split into CRLF-separated chunks
        # with a leading space on every continuation line.
        encoded = line.encode("utf-8")
        parts = []
        while len(encoded) > limit:
            # Cut without splitting a multi-byte UTF-8 character.
            cut = limit
            while (encoded[cut] & 0xC0) == 0x80:  # UTF-8 continuation byte
                cut -= 1
            parts.append(encoded[:cut].decode("utf-8"))
            encoded = encoded[cut:]
        parts.append(encoded.decode("utf-8"))
        return "\r\n ".join(parts)
