# SPDX-FileCopyrightText: 2024-present Helder Guerreiro <helder@tretas.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

import sys
from datetime import datetime, timedelta

import click
from caldav.lib import error

from caldavctl import dav, get_name
from caldavctl.click_utils import create_compatibility_check
from caldavctl.event_builder import event_builder, parse_event
from caldavctl.object_parser import ParserError
from caldavctl.templates import render
from caldavctl.utils import calmonth, edit_text_with_editor, to_datetime

# Configuration

EMPTY_EVENT = '''# Mandatory
DTSTART: %(start)s
DTEND: %(end)s

%(cal)s

# Recommended
SUMMARY:

# Optional
LOCATION:
CATEGORIES:
TIMEZONE:
PRIORITY: 5
ALARM: P0D
DESCRIPTION: [[ ]]

# NOTES:
#
#   * Date and time:
#       * The dates must be in iso format, for instance: 2024-12-29 13:45;
#       * The timezone used is the one defined by default or the one defined in
#         the "TIMEZONE" key;
#       * You can use the command "caldavctl utils list-timezones" to list the
#         available time zones;
#   * Categories: the categories are a comma separated list;
#   * Priority: 1 - highest priority, 9 - lowest priority
#   * Description: The description can be multi line, just make sure it's
#     delimited by [[ ]].
#   * Alarms:
#       * By default an alarm is defined the moment the event begins;
#       * The alarms are defined using:
#           ALARM: <trigger> [<description>]
#       * The trigger is defined according to:
#           https://datatracker.ietf.org/doc/html/rfc5545#autoid-38
#       * For example, to trigger an alarm 30 minutes before the event start, use:
#           ALARM: -PT30M
#       * The alarm description is optional
'''


# Commands:

exclusive_option = create_compatibility_check(
    ['today', 'week', 'dtstart', 'dtend', 'day'],  # Exclusive options
    [['dtstart', 'dtend']])  # Except these two that can be used together


@click.command('list', options_metavar='[options]')
@click.option('-t', '--today',
              is_flag=True, show_default=True, default=False,
              callback=exclusive_option, help='Today\'s events',)
@click.option('-w', '--week',
              default=None, metavar='<offset>', type=int,
              callback=exclusive_option, help='Week events')
@click.option('-s', '--dtstart',
              callback=exclusive_option, help='Date range, start date', metavar='<yyyy-mm-dd>')
@click.option('-e', '--dtend',
              callback=exclusive_option, help='Date range, end date', metavar='<yyyy-mm-dd>')
@click.option('-d', '--day',
              callback=exclusive_option, help='Events on the day', metavar='<yyyy-mm-dd>')
@click.option('-sd', '--show-description',
              is_flag=True, show_default=True, default=False,
              help='Show the event\'s description.')
@click.option('-si', '--show-uid',
              is_flag=True, show_default=True, default=False,
              help='Show the event\'s UID.')
@click.option('-tf', '--template-file', default='event.txt',
              help='Template used to format the output', metavar='<file>')
@click.pass_obj
def list_events(context, today, week, dtstart, dtend, day, show_description, show_uid, template_file):
    '''
    List events from the server list

    The date range can be specified in several ways. The options are self
    explanatory except the --week option. This option takes an integer
    argument, if we use '0' the time range will be the current week. If we use
    '1' the time range will correspond to next week, likewise, '-1' corresponds
    to last week or '-2' to two weeks ago.
    '''
    tz = context['config'].tz()
    start_date = None
    end_date = None

    if today:
        start_date = datetime.now().replace(hour=0, minute=0, second=0, tzinfo=tz)
        end_date = datetime.now().replace(hour=23, minute=59, second=59, tzinfo=tz)
    elif week is not None:
        today_date = datetime.now().replace(hour=0, minute=0, second=0, tzinfo=tz)
        start_date = today_date - timedelta(days=today_date.weekday())
        end_date = start_date + timedelta(days=6)
        end_date = end_date.replace(hour=23, minute=59, second=59)
        # Apply week offset
        start_date += timedelta(weeks=week)
        end_date += timedelta(weeks=week)
    elif dtstart and dtend:
        if dtstart:
            start_date = to_datetime(dtstart, tz)
        else:
            start_date = datetime(1900, 1, 1, tzinfo=tz)
        if dtend:
            end_date = to_datetime(dtend, tz)
        else:
            end_date = datetime(5000, 1, 1, tzinfo=tz)
    elif day:
        try:
            dt = datetime.fromisoformat(day)
            start_date = dt.replace(hour=0, minute=0, second=0, tzinfo=tz)
            end_date = dt.replace(hour=23, minute=59, second=59, tzinfo=tz)
        except ValueError:
            raise click.UsageError(f'Invalid date "{day}"')
    else:
        raise click.UsageError('You have to specify a time frame, for example with -t or -w.')

    _, server = context['config'].get_server()
    calendar_id = context['config'].get_calendar()
    with dav.caldav_calendar(server, calendar_id) as calendar:
        # Check if the calendar supports events:
        if 'VEVENT' not in calendar.get_supported_components():
            raise click.UsageError('This calendar does not support events.')

        # Get events in time range
        events = calendar.search(
            start=start_date,
            end=end_date,
            event=True,
            expand=True,
            sort_keys=['dtstart']
        )
        event_list = []
        for event in events:
            ev = event.icalendar_component
            date = ev.get('dtstart').dt
            event_context = {
                'date': date.strftime('%Y-%m-%d %H:%M') if isinstance(date, datetime) else f'{date} (day)',
                'summary': ev.get('summary', ''),
                'description': ev.get('description', ''),
                'uid': ev.get('uid')
            }
            event_list.append(event_context)

        template_context = {
            'show_uid': show_uid,
            'show_description': show_description,
            'event_list': event_list,
            'newline': '\n'
        }

        txt = render(get_name(), template_file, template_context)
        click.echo(txt.strip())


@click.command('create', options_metavar='[options]')
@click.option('-f', '--file', type=click.File('r'), default=sys.stdin, help='Create event from this file', metavar='<file>')
@click.option('-e', '--edit', is_flag=True, default=False, help='Edit the event on a text editor?')
@click.option('--dry-run', is_flag=True, default=False, help='Show only the iCalendar generated')
@click.pass_obj
def create_event(context, file, edit, dry_run):
    '''
    Create new event

    The event data can be read from stdin or from a file. Optionally the event
    can be edited using the default $EDITOR. If the option to edit is enabled
    and no file is defined using -f/--file, then an empty event is opened.
    '''
    tz = context['config'].tz()
    # Read the event definition
    if edit and file is sys.stdin:
        tomorrow = datetime.now() + timedelta(days=1)
        event_string = EMPTY_EVENT % {
            'tz': f'{tz}',
            'start': tomorrow.replace(hour=9, minute=0, second=0).strftime('%Y-%m-%d %H:%M'),
            'end': tomorrow.replace(hour=9, minute=45, second=0).strftime('%Y-%m-%d %H:%M'),
            'cal': '\n'.join([('# ' + ln).strip() for ln in calmonth(tomorrow.year, tomorrow.month, 3)])
        }
    else:
        event_string = file.read()

    if edit:
        # Edit the file in $VISUAL or $EDITOR
        old_string = event_string
        event_string = edit_text_with_editor(event_string, suffix='.calendar_event')
        if old_string == event_string:
            click.confirm('No changes made. Do you want to continue?', abort=True)

    try:
        event_data = parse_event(event_string, tz)
    except ParserError as msg:
        raise click.UsageError(msg)

    event_ics = event_builder(event_data, tz)

    if dry_run:
        click.echo(event_ics)
    else:
        # Save the event to the calendar
        _, server = context['config'].get_server()
        calendar_id = context['config'].get_calendar()
        with dav.caldav_calendar(server, calendar_id) as calendar:
            # Check if the calendar supports events:
            if 'VEVENT' not in calendar.get_supported_components():
                raise click.UsageError('This calendar does not support events.')
            try:
                calendar.save_event(event_ics)
            except error.AuthorizationError as msg:
                raise click.UsageError(f'Error saving the event to the server: {msg}')

        click.echo('Event created successfully')


@click.command('delete', options_metavar='[options]')
@click.argument('event_id', metavar='<uid>')
@click.pass_obj
def delete_event(context, event_id):
    '''Delete an event on the server'''
    _, server = context['config'].get_server()
    calendar_id = context['config'].get_calendar()

    with dav.caldav_calendar_event(server, calendar_id, event_id) as event:
        event.delete()
    print('Event deleted')
