import logging, yaml, pprint, json, re
import hashlib, uuid
import caldav
import datetime as dt

from . import login, debug

_l = logging.getLogger(__name__)

def buildSubparser(subparser):
    subparser.add_argument('--schedule', default='../../data/schedule.yaml')
    subparser.add_argument('--holidays', default='../../data/holidays.yaml')
    subparser.add_argument('-n', '--dry', action='store_true')
    subparser.add_argument('--recreate-all', action='store_true')

def run(args):
    loginData, schedule, holidays = _loadRawData(args)
    packedSchedule = _unpackSchedules(schedule)
    fixedSchedule = _addHolidayExceptions(packedSchedule, holidays)
    _synchonizeCalendars(args, fixedSchedule, loginData, holidays)
    
def _loadRawData(args):
    _l.info('Loading data from hard disk')
    loginData = login.loadLoginData()

    with open(args.schedule, 'r') as f:
        schedule = yaml.safe_load(f.read())
    
    _l.log(5, 'Schedule data:\n%s', pprint.pformat(schedule, indent=4, width=100))

    with open(args.holidays, 'r') as f:
        holidays = yaml.safe_load(f.read())
    
    _l.log(5, 'Holidays data:\n%s', pprint.pformat(holidays, indent=4, width=100))

    _l.info('Data was read from hard disk')
    
    return loginData, schedule, holidays

class Event:
    def __init__(self, day, start, duration, title):
        self.day = day
        self.start = start
        self.duration = duration
        self.title = title
        self.age = None
        self.external = False
        self.description = ''
        self.exceptions = []
    
    def __repr__(self):
        wAge = f' ({self.age})' if self.age is not None else ''
        ext = 'Ext' if self.external else ''
        return f'{ext}Ev({self.title}{wAge}) [{self.day}, {self.start}, {self.duration}]'
    
    def getHash(self, holidays):
        def fixHolidays(holidays):
            def fixDate(d):
                return d.strftime('%Y-%m-%d')
            
            def fixFeast(f):
                return fixDate(f)
            
            def fixHoliday(h):
                return {
                    'from': fixDate(h['from']),
                    'to': fixDate(h['to']),
                }

            return {
                **holidays,
                'feasts': [fixFeast(f) for f in holidays['feasts']],
                'holidays': [fixHoliday(h) for h in holidays['holidays']],
            }

        data = {
            'day': self.day,
            'start': self.start,
            'duration': self.duration,
            'title': self.title,
            'age': self.age,
            'external': self.external,
            'description': self.description,
            'exceptions': self.exceptions,
            "holidays": fixHolidays(holidays),
        }
        # pprint.pprint(data, indent=4, width=100)
        hasher = hashlib.sha1()
        hasher.update(json.dumps(data, sort_keys=True).encode('utf-8'))
        first = hasher.hexdigest()
        second = uuid.uuid4().hex
        return f'{first}___{second}', first
    
    dDay = dt.timedelta(days=1)

    _MAP_WEEKDAY_ICAL = {
        'Mon': 'MO',
        'Tue': 'TU',
        'Wed': 'WE',
        'Thu': 'TH',
        'Fri': 'FR',
        'Sat': 'SA',
        'Sun': 'SU',
    }

    def addToCalendar(self, calendar: caldav.Calendar, holidays):
        from . import cal_helper

        def getFirstTimeInYear():
            now = dt.datetime.now()
            d = dt.datetime(year=now.year, month=1, day=1)
            pass

        helper = cal_helper.IcalHelper()
        startDay, start = helper.getStart(self, holidays)

        if start is None:
            _l.warning('Could not get start time for event %s in the current year', self)
            return
        
        uid, uidFirst = self.getHash(holidays)
        icalData = {
            'uid': uid,
            'DTSTART': start,
            'DTEND': start + dt.timedelta(minutes=self.duration),
            'SUMMARY': self.title,
            'RRULE': {
                'FREQ': 'DAILY',
                'BYDAY': self._MAP_WEEKDAY_ICAL[self.day]
            }
        }
        # debug.debugger()

        holidayExceptions = helper.getHolidayExceptions(self, startDay, holidays)
        if holidayExceptions is not None:
            icalData['EXDATE'] = holidayExceptions

        desc = ''
        if self.age is not None:
            desc += f'Jahrgänge: {self.age}'
        if len(self.description) > 0:
            desc += '\n\n'
            desc += self.description
        if len(desc) > 0:
            icalData['DESCRIPTION'] = desc

        _l.log(5, 'Adding event\n%s', pprint.pformat(icalData, indent=4, width=100))

        ical = caldav.lib.vcal.create_ical(**icalData)
        _l.log(5, 'Created event\n%s', pprint.pformat(ical, indent=4, width=100))
        _l.log(5, 'Created event\n%s', ical)
        calendar.add_event(**icalData)

def _unpackSchedules(schedule):
    def packSingleCalendar(cal):
        def parseSingleEvent(ev):
            e = Event(
                day=ev['day'],
                start=ev['start'],
                duration=ev['duration'],
                title=ev['title']
            )

            if 'age' in ev:
                e.age = ev['age']
            if ev.get('extern', False) or ev.get('external', False):
                e.external = True
            if 'desc' in ev:
                e.description = ev['desc']
            if 'exceptions' in ev:
                raise Exception('Not yet implemented to have exceptions')
            
            return e

        _l.log(5, 'Unpacking calendar %s', cal)
        ret = { **cal }

        if 'schedule' in ret:
            ret['schedule'] = [
                parseSingleEvent(e) for e in ret['schedule']
            ]

        _l.log(5, 'Unpacked calendar %s', ret)
        return ret

    ret = {}

    for calName in schedule['calendars']:
        if schedule['calendars'][calName].get('ignore', False):
            _l.info('Ignoring calendar %s', calName)
            continue

        ret[calName] = packSingleCalendar(schedule['calendars'][calName])
    
    _l.log(5, 'Unpacked schedule:\n%s', pprint.pformat(ret, indent=4, width=100))
    return ret

def _addHolidayExceptions(schedule, holidays):
    return schedule

class CalendarSynchonizer:
    def __init__(self, args, calId, calName, loginData):
        self.args = args
        self.calId = calId
        self.calName = calName
        self.loginData = loginData
        self._calDav = None
    
    def _getUrl(self):
        return f'{self.loginData.base}/remote.php/dav/calendars'

    def _getCalDav(self):
        
        if self._calDav is None:
            self._calDav = caldav.DAVClient(
                url=self._getUrl(),
                username=self.loginData.loginName,
                password=self.loginData.appPassword
            )
        return self._calDav
    
    def _getCalendar(self):
        cd = self._getCalDav()
        return cd.principal().calendar(cal_id=self.calId)

    def synchonize(self, calendars, holidays):
        downstreamEvents = self._getDownstreamEvents(calendars[self.calName]['schedule'], holidays)
        _l.debug('Downstream events:\n%s', pprint.pformat(downstreamEvents, indent=4, width=100))

        upstreamEvents = self._getUpstreamEvents()
        _l.debug('Upstream events:\n%s', pprint.pformat(upstreamEvents, indent=4, width=100))

        newEvents = self._getNewEvents(upstreamEvents, downstreamEvents)
        _l.log(5, 'New events:\n%s', pprint.pformat(newEvents, indent=4, width=100))
        deletedEvents = self._getDeletedEvents(upstreamEvents, downstreamEvents)
        _l.log(5, 'Events marked for deletion:\n%s', pprint.pformat(deletedEvents, indent=4, width=100))

        self._deleteEvents(deletedEvents)
        self._addEvents(newEvents, holidays)

    def _getUpstreamEvents(self):
        _l.debug('Fetching upstream calendar entries')
        cd = self._getCalDav()
        
        # principal = cd.principal()
        # _l.log(5, 'Principal %s', principal)

        # calendar = principal.calendar(cal_id=self.calId)
        # _l.log(5, 'Calendar %s', calendar)

        calendar = self._getCalendar()

        # children = calendar.children()
        # _l.debug('Getting children for calendar %s:\n%s', self.calId, children)

        events = calendar.events()

        ret = {}

        for e in events:
            uidRaw = str(e.icalendar_component['uid'])
            uid = uidRaw.split('___', 1)[0]
            _l.log(5, 'Event with uid %s was found.', uid)
            ret[uid] = e

        return ret
    
    def _getDownstreamEvents(self, schedule, holidays):
        _l.debug('Preparing local calendar events')
        
        ret = {}

        for e in schedule:
            uid, uidFirst = e.getHash(holidays)
            _l.log(5, 'Event with uid part %s was found.', uidFirst)
            ret[uidFirst] = e

        return ret
    
    def _getNewEvents(self, upstreamEvents, downstreamEvents):
        if self.args.recreate_all:
            return downstreamEvents.values()
        
        upstreamUids = set(upstreamEvents.keys())
        downstreamUids = set(downstreamEvents.keys())
        newUids = downstreamUids - upstreamUids

        return [downstreamEvents[uid] for uid in newUids]
    
    def _getDeletedEvents(self, upstreamEvents, downstreamEvents):
        if self.args.recreate_all:
            return upstreamEvents.values()
        
        upstreamUids = set(upstreamEvents.keys())
        downstreamUids = set(downstreamEvents.keys())
        toDeleteUids = upstreamUids - downstreamUids

        return [upstreamEvents[uid] for uid in toDeleteUids]

    def _addEvents(self, newEvents, holidays):
        cal = self._getCalendar()

        for e in newEvents:
            _l.debug('Adding event %s', e)
            if self.args.dry:
                _l.info('Skipping add event %s (dry mode)', e)
            else:
                # e.save()
                e.addToCalendar(cal, holidays)
    
    def _deleteEvents(self, oldEvents):
        for e in oldEvents:
            _l.debug('Deleting event %s', e)
            if self.args.dry:
                _l.info('Skipping delete event %s (dry mode)', e)
            else:
                e.delete()

def _synchonizeCalendars(args, calendars, loginData, holidays):
    for calName in calendars:
        calendar = calendars[calName]
        _l.info('Synching calendar %s', calName)
        calendarSynchonizer = CalendarSynchonizer(args, calendar['id'], calName, loginData)
        calendarSynchonizer.synchonize(calendars, holidays)