#!/usr/bin/env python3
"""Print the number of hours specified on command line.

Every week at work, we sign up for the shifts we want to work *next*
week. They print out the shifts they need each day in a schedule for
the week in a grid: Sunday through Saturday across, time boxes going down.

We select our shifts by signing our name in the boxes on the grid. The
person with the most seniority picks first, etc. Sometimes, when it's my
turn to pick (I'm 4th out of 9 in line) and I'm not at work, they'll text
me a photo of the schedule, and I'll text them back with a string like:

    "Sun 11:15-4; Mon 12-4:30, Tue 215-8; Wed off; Thu off; Fri 12-5; Sat 2:15-8"

This program that takes that string, and computes the number of hours
being signed up for.

The --gcalcli option takes that string, and creates events in a google calendar.
"""

import re
import sys
import time
from collections import OrderedDict


class Event:
    """A calendar event."""

    # pylint: disable=too-few-public-methods

    when: int = 0  # number of minutes since midnight.
    duration: int = 0  # number of minutes.
    spec: str = ""  # original input string.

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            + ", ".join(
                [
                    f"when={Hours.format_time(self.when)!r}",
                    f"duration={Hours.format_time(self.duration)!r}",
                    f"spec={self.spec!r}",
                ]
            )
            + ")"
        )


class Hours:
    """Print the number of hours specified on command line."""

    # Match timespec formatted as `HHMM` (3 or 4 digits, no colon).
    # e.g. 145 for 01:45, 200 for 02:00, 430 for 04:30, 1245 for 12:45
    re_hhmm_sans_colon = re.compile(r"(?P<hh>\d{1,2})(?P<mm>\d{2})")

    # Match timespec formatted as `HH:MM` (1-4 digits, with colon).
    # e.g. 1 for 01:00, 1:30 for 01:30,
    re_hhmm_avec_colon = re.compile(r"(?P<hh>\d{1,2})(?::(?P<mm>\d{1,2}))?")

    daynames = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

    @staticmethod
    def format_time(seconds: int) -> str:
        """Return `seconds` formatted as `HH:MM`."""
        return f"{seconds // 60:02}:{seconds % 60:02}"

    def parse_time(self, string: str) -> int:
        """Return the number of minutes specified in `string` string."""

        if m := self.re_hhmm_sans_colon.fullmatch(string):
            hh = int(m["hh"])
            mm = int(m["mm"])

        elif m := self.re_hhmm_avec_colon.fullmatch(string):
            hh = int(m["hh"])
            mm = int(m["mm"]) if m["mm"] is not None else 0

        else:
            raise ValueError(f"Can't parse {string!r}")

        # PLR2004: 23/59 are the maximum valid hour/minute values in the time format.
        if hh > 23:  # noqa: PLR2004
            raise ValueError(f"Invalid hours in {string!r}")
        if mm > 59:  # noqa: PLR2004
            raise ValueError(f"Invalid minutes in {string!r}")
        return (hh * 60) + mm

    def parse(self, string: str) -> dict[str, Event]:
        """Return the number of hours specified in `string`."""

        week = OrderedDict({x: Event() for x in self.daynames})

        for part in re.split(r"[;,./]+", string):  # "Sun 11:15-4"
            if not (part := part.strip()):
                break

            dayname, spec = part.split(" ")  # ["Sun", "11:15-4"]

            if (dayname := dayname.title()) not in self.daynames:
                raise ValueError(f"Invalid day {dayname!r}")

            if spec == "off":
                continue

            from_time, to_time = spec.split("-")  # ["11:15", "4"]
            from_mins = self.parse_time(from_time)
            to_mins = self.parse_time(to_time)

            if from_mins < (7 * 60):
                from_mins += 12 * 60

            if to_mins < from_mins:
                to_mins += 12 * 60

            week[dayname].when = from_mins
            week[dayname].duration = to_mins - from_mins
            week[dayname].spec = spec

        return week

    def report(self, week: dict[str, Event]) -> None:
        """Print simple report."""

        total = 0
        for dayname in self.daynames:
            event = week[dayname]
            if not event.when:
                print(f"{dayname} off")
                continue

            total += event.duration
            print(
                " ".join(
                    [
                        dayname,
                        self.format_time(event.duration),
                        "-".join(
                            [
                                self.format_time(event.when),
                                self.format_time(event.when + event.duration),
                            ]
                        ),
                        "parsed from",
                        repr(event.spec),
                    ]
                )
            )

        print("--- -----")
        print(f"Tot {self.format_time(total)}")

    def gcalcli(self, start_date: str, week: dict[str, Event]) -> None:
        """Print `shell` commands to add events to google-calendar.

        `start_date` defines the date for the first day of the week, Sunday.
        """

        date = int(time.mktime(time.strptime(start_date, "%Y-%m-%d")))

        for dayname in self.daynames:
            event = week[dayname]
            if event.duration:
                print(
                    "gcalcli",
                    "add",
                    "--calendar",
                    "russel.lane@gmail.com",
                    "--title",
                    repr("Fry's"),
                    "--when",
                    time.strftime('"%Y-%m-%d %H:%M"', time.localtime(date + (event.when * 60))),
                    "--duration",
                    event.duration,
                    "--reminder",
                    "30m",
                    # "--where",
                    # repr(""),
                    # "--description",
                    # repr(""),
                    "--noprompt",
                )
            date += 86400

    def main(self) -> None:
        """Print the number of hours specified in `argv[1]`."""

        # PLR2004: 2 is the expected argc for normal invocation (prog + one argument).
        if (argc := len(sys.argv)) > 2 and sys.argv[1] == "--gcalcli":  # noqa: PLR2004
            self.gcalcli(sys.argv[2], self.parse(sys.argv[3]))
        elif argc == 2:  # noqa: PLR2004
            self.report(self.parse(sys.argv[1]))
        else:
            print('Usage: hours [--gcalcli START-DATE] "Mon 10-2, Tue off, Wed 12-4:30, etc."')
            sys.exit(2)


def main() -> None:
    """CLI entry point."""
    Hours().main()


if __name__ == "__main__":
    main()

# eg: "Sun off. Mon 12-5. Tue 130-530. Wed 12-415. Thu off. Fri 12-445. Sat 2-6"
# eg: "Sun 1-5, Wed 1-5, Thu 12-4, Fri 12-4:45, Sat 12-4"
# eg: "Sun 2-8 // Mon 10:30-4:45 // Tue 10-4 // Wed 11:15-3:15 // Thu 10:30-2:30 // Fri off // Sat off"  # noqa: E501
# eg: hours --gcalcli 2024-04-21 "Sun 11:15-4; Mon 12-4:30, Tue 2:15-8; Wed off; Thu off; Fri 12-5; Sat 2:15-8"  # noqa: E501
# eg: hours --gcalcli 2024-04-28 "Sun 12:30-4:30, Mon 12:45-4:45, Tue 10-2:30, Wed 12-4, Thu off, Fri 12:45-4:45, Sat off"  # noqa: E501
