from dataclasses import dataclass
from multiprocessing import Process, Event as event, Queue
from multiprocessing.synchronize import Event
from pathlib import Path
from signal import SIGTERM, SIGINT, signal
from tempfile import mkdtemp
from typing import Optional, List
from warnings import simplefilter

import click
from click_path import PathlibPath
import cryptography

from boba_fetch.poller import RemotePoller
from boba_fetch.rpc import UnixSocketRPCClient
from boba_fetch.syncer import Syncer, WatchParams
from boba_fetch.tracker import Tracker


simplefilter('ignore', cryptography.utils.CryptographyDeprecationWarning)


@dataclass
class BobaConfig:
    remote_host: str
    remote_dir: Path
    local_dir: Path
    staging_dir: Path
    tracking_dir: Path
    num_sync_workers: int
    delete_src: bool
    execute_on_complete: Optional[str]
    polling_delay: float

    def to_watch_params(self, queue: Queue) -> WatchParams:
        return WatchParams(self.remote_host, self.local_dir, queue, self.delete_src,
                           self.execute_on_complete)


def run_poller(config: BobaConfig,
               tracker: UnixSocketRPCClient,
               queue: Queue,
               halt_flag: Event):
    poller = RemotePoller(config.remote_host, tracker)
    poller.poll(config.polling_delay, config.remote_dir, queue, halt_flag)


def start_poller(config: BobaConfig,
                 tracker: UnixSocketRPCClient,
                 queue: Queue,
                 halt_flag: Event) -> Process:
    poller_proc = Process(target=run_poller, args=(config, tracker, queue, halt_flag))
    poller_proc.start()
    return poller_proc


def run_syncer(config: BobaConfig,
               tracker: UnixSocketRPCClient,
               queue: Queue,
               halt_flag: Event):
    syncer = Syncer(tracker, halt_flag, config.staging_dir)
    syncer.watch_queue(config.to_watch_params(queue))


def start_syncers(config: BobaConfig,
                  tracker: UnixSocketRPCClient,
                  queue: Queue,
                  halt_flag: Event) -> List[Process]:
    syncer_procs = [Process(target=run_syncer,
                            args=(config, tracker,
                                  queue, halt_flag)) for _ in range(config.num_sync_workers)]
    for proc in syncer_procs:
        proc.start()
    return syncer_procs


@click.command()
@click.option('-r', '--remote', required=True,
              help='The remote host to poll for file updates. Must be SSH accessible via public key '
                   'and configured in your ~/.ssh/config')
@click.option('-s', '--src-dir', required=True, type=PathlibPath(exists=False),
              help='The directory on the remote host to check for files to download')
@click.option('-d', '--dst-dir', required=True, type=PathlibPath(exists=True, file_okay=False,
                                                                 dir_okay=True, writable=True,
                                                                 resolve_path=True),
              help='Local directory to sync the files to from the remote host')
@click.option('-g', '--staging-dir', default=None, type=PathlibPath(exists=True, file_okay=False,
                                                                    dir_okay=True, writable=True,
                                                                    resolve_path=True),
              help='Staging directory where files are saved, defaults to randomized temporary directory')
@click.option('-t', '--tracking-dir', required=True, type=PathlibPath(exists=True, file_okay=False,
                                                                      dir_okay=True, writable=True,
                                                                      resolve_path=True),
              help='Tracking directory where we keep track of files that have been synced')
@click.option('-n', '--num-sync-workers', default=3, type=int,
              help='Number of sync workers to use for downloads, defaults to 3')
@click.option('--delete-src', is_flag=True, default=False, help='Indicates to whether to delete the source files on '
                                                                'the remote system')
@click.option('-e', '--execute-on-complete', default=None, help='A command to execute every time a sync is complete')
@click.option('-p', '--polling-delay', default=30.0, type=float,
              help='Number of seconds to wait between polls of the remote directory, defaults to 30')
def main(remote: str,
         src_dir: Path,
         dst_dir: Path,
         staging_dir: Optional[Path],
         tracking_dir: Path,
         num_sync_workers: int,
         delete_src: bool,
         execute_on_complete: Optional[str],
         polling_delay: float):
    queue: Queue = Queue()
    halt_flag = event()
    config = BobaConfig(remote, src_dir, dst_dir, staging_dir or Path(mkdtemp()),
                        tracking_dir, num_sync_workers, delete_src, execute_on_complete,
                        polling_delay)
    with Tracker(config.tracking_dir).proxy() as tracker:
        syncer_procs = start_syncers(config, tracker, queue, halt_flag)
        poller_proc = start_poller(config, tracker, queue, halt_flag)

        def handle_signal(_, __):
            halt_flag.set()
        signal(SIGTERM, handle_signal)
        signal(SIGINT, handle_signal)
        poller_proc.join()
        print("Poller shutdown complete.")
        for sync_proc in syncer_procs:
            sync_proc.join()
        print("Syncer shutdown complete.")
        print("Done.")
