#!/bin/python3

import logging
import collections
import datetime
import argparse
import enum
import json

class DateType(enum.Enum):
    CUSTOM = "custom"
    STATIC = "static"
    CYCLICAL = "cyclical"

    def __str__(self):
        return self.name.lower()

    def __repr__(self):
        return str(self)

    @staticmethod
    def argparse(s):
        try:
            return DateType[s.upper()]
        except KeyError:
            return s


args_parser = argparse.ArgumentParser(description="special events checker")
args_parser.add_argument("path", type=str, help="event json file path")
args_parser.add_argument(
    "type",
    type=DateType.argparse,
    choices=[t for t in DateType],
    help="event date type",
)

args = args_parser.parse_args()

special_events: dict[str, list[dict]] = {}

try:
    with open(args.path) as f:
        special_events = json.loads(f.read())
except json.JSONDecodeError:
    print(f"`{args.path}` json syntax error.")
    exit(-1)

except FileNotFoundError:
    print(f"`{args.path}` not found.")
    print("Please contact developer to solve this problem.")
    exit(-1)

if not isinstance(special_events, dict):
    print("`special_events` should be a dict")
    exit(-1)

if "special_events" not in special_events:
    print(f"`special_events` not found in `{args.path}`.")
    exit(-1)

if not isinstance(special_events["special_events"], list):
    print(f"`special_events` in `{args.path}` should be a list.")
    exit(-1)

MIN_STATUS_INDEX = 0
MAX_STATUS_INDEX = 7
DAYSPERMONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

errors: dict[int, list[str]] = collections.defaultdict(list)


def is_leap_year(year: int) -> bool:
    """Determines whether a given year is a leap year.

    Args:
        year (int): The year to check.

    Returns:
        bool: True if the year is a leap year, False otherwise.
    """

    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    if year % 4 == 0:
        return True

    return False


def validate_number(event_idx: int, value, min: int, max: int, field_name: str) -> int | None:
    """Validates whether a given value is an integer within a specified range.

    Args:
        event_idx (int): The index of the event for associating validation errors.
        value (Any): The value to validate.
        min (int): The minimum acceptable value (inclusive).
        max (int): The maximum acceptable value (inclusive).
        field_name (str): The name of the field being validated, used in error messages.

    Returns:
        int | None: The validated integer value if it is within the range, otherwise None.

    Raises:
        ValueError: If `value` cannot be converted to an integer.

    Validation Rules:
        - If `value` cannot be converted to an integer, an error is recorded and None is returned.
        - If `value` is outside the range defined by `min` and `max`, an error is recorded and None is returned.
    """

    try:
        value = int(value)
    except ValueError:
        errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
        return None

    if value < min or value > max:
        errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
        return None

    return value


def require_field_check(
    obj: dict, event_idx: int, fields: list[tuple[str, type]], required_field: str = ""
) -> bool:
    """
    Validates the presence and types of required fields in a given object.

    Args:
        obj (dict): The object (dictionary) to validate.
        event_idx (int): The index of the event for associating validation errors.
        fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type.
        required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "".

    Returns:
        bool: True if all required fields are present and have the correct types, otherwise False.

    Validation Rules:
        - If a required field is missing, an error message is recorded.
        - If a field is present but its type does not match the expected type, an error message is recorded.
        - The `required_field` parameter, if provided, is prepended to error messages for context.
    """

    error_found = False
    for field_name, field_type in fields:
        if field_name not in obj:
            error_found = True
            msg = ""
            if required_field != "":
                msg = f"`{required_field}` "

            msg += f"missing `{field_name}`."
            errors[event_idx].append(msg)

        elif not isinstance(obj[field_name], field_type):
            error_found = True
            errors[event_idx].append(f"`{field_name}` should be a `{field_type}` type.")

    if error_found:
        return False
    return True


event_names = set()
event_dates = set()


def check_structure(event, idx: int):
    if not isinstance(event, dict):
        errors[idx].append("should be a dict")
        return False

    if not require_field_check(
        event,
        idx,
        [
            ("event", str),
            ("triggerDate", dict),
            ("status_index", str),
            ("goodFortunes", dict),
            ("badFortunes", dict),
        ],
    ):
        return False

    event_name: str = event["event"]
    if event_name.strip() == "":
        errors[idx].append("event name should not empty.")
        return

    if event_name in event_names:
        errors[idx].append(f"event `{event_name}` already exists.")

    validate_number(
        idx, event["status_index"], MIN_STATUS_INDEX, MAX_STATUS_INDEX, "status_index"
    )

    if require_field_check(
        event["goodFortunes"],
        idx,
        [
            ("l_1_event", str),
            ("l_1_desc", str),
            ("l_2_event", str),
            ("l_2_desc", str),
        ],
        "goodFortunes"
    ):
        if bool(event["goodFortunes"]["l_1_event"]) ^ bool(event["goodFortunes"]["l_1_desc"]):
            # Check for inconsistency: XOR is used to ensure both l_1_event and l_1_desc
            # are either both provided or both missing. If only one is provided, log an error.
            errors[idx].append("First good fortune is incomplete.")

        if bool(event["goodFortunes"]["l_2_event"]) ^ bool(event["goodFortunes"]["l_2_desc"]):
            # Check for inconsistency: XOR is used to ensure both l_2_event and l_2_desc
            # are either both provided or both missing. If only one is provided, log an error.
            errors[idx].append("Second good fortune is incomplete.")

    if require_field_check(
        event["badFortunes"],
        idx,
        [
            ("r_1_event", str),
            ("r_1_desc", str),
            ("r_2_event", str),
            ("r_2_desc", str),
        ],
        "badFortunes"
    ):
        if bool(event["badFortunes"]["r_1_event"]) ^ bool(event["badFortunes"]["r_1_desc"]):
            # Check for inconsistency: XOR is used to ensure both r_1_event and r_1_desc
            # are either both provided or both missing. If only one is provided, log an error.
            errors[idx].append("First bad fortune is incomplete.")

        if bool(event["badFortunes"]["r_2_event"]) ^ bool(event["badFortunes"]["r_2_desc"]):
            # Check for inconsistency: XOR is used to ensure both r_2_event and r_2_desc
            # are either both provided or both missing. If only one is provided, log an error.
            errors[idx].append("Second bad fortune is incomplete.")

    event_names.add(event_name)

    return True

def check_static_date(event: dict, idx: int):
    trigger_date: dict = event["triggerDate"]
    corrected = require_field_check(
        trigger_date,
        idx,
        [
            ("month", str),
            ("date", str),
        ],
        "triggerDate",
    )

    event_name: str = event["event"]
    if "year" in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `custom_special.json`."
        )

    if "week" in trigger_date or "weekday" in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `cyclical_special.json`."
        )

    if not corrected:
        return

    month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
    if month is not None:
        validate_number(
            idx, trigger_date["date"], 1, DAYSPERMONTH[month], "triggerDate.date"
        )


def check_cyclical_date(event: dict, idx: int):
    trigger_date: dict = event["triggerDate"]
    corrected = require_field_check(
        trigger_date,
        idx,
        [
            ("month", str),
            ("week", str),
            ("weekday", str),
        ],
        "triggerDate",
    )

    event_name: str = event["event"]
    if "year" in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `custom_special.json`."
        )

    elif "date" in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `static_special.json`."
        )

    if not corrected:
        return

    validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
    validate_number(idx, trigger_date["week"], 1, 5, "triggerDate.week")
    validate_number(idx, trigger_date["weekday"], 1, 7, "triggerDate.weekday")


def check_custom_date(event: dict, idx: int):
    trigger_date: dict = event["triggerDate"]
    corrected = require_field_check(
        trigger_date,
        idx,
        [
            ("year", str),
            ("month", str),
            ("date", str),
        ],
        "triggerDate",
    )

    event_name: str = event["event"]
    if "week" in trigger_date or "weekday" in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `cyclical_special.json`.",
        )

    elif "year" not in trigger_date:
        errors[idx].append(
            f"this event `{event_name}` should be placed in `static_special.json`."
        )

    if not corrected:
        return

    year = validate_number(
        idx,
        trigger_date["year"],
        datetime.datetime.min.year,
        datetime.datetime.max.year,
        "triggerDate.year",
    )
    month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")

    if year is None or month is None:
        return

    days = DAYSPERMONTH[month]
    if month == 2 and is_leap_year(year):
        days += 1  # 29

    date = validate_number(idx, trigger_date["date"], 1, days, "triggerDate.date")
    if date is None:
        return

    date_str = f"{year}/{month}/{date}"
    if date_str in event_dates:
        errors[idx].append(f"The date `{date_str}` of `{event_name}` is repeated.")

    event_dates.add(date_str)


date_checker = {
    DateType.CUSTOM: check_custom_date,
    DateType.STATIC: check_static_date,
    DateType.CYCLICAL: check_cyclical_date,
}
check_triggerdate = date_checker[args.type]

for idx, event in enumerate(special_events["special_events"]):
    if check_structure(event, idx):
        check_triggerdate(event, idx)

if errors:
    logging.error(args.path)
    for idx, error_msgs in errors.items():
        logging.error(json.dumps(special_events["special_events"][idx], indent=4, ensure_ascii=False))
        for msg in error_msgs:
            logging.error(msg)
    exit(-1)
