import argparse
import os
import signal
import sys
import time
from dataclasses import dataclass
from typing import Protocol, Tuple

from fastq_handler.actions import ProcessActionDownsize, ProcessActionMergeWithLast

from findontime.configs import InfluConfig, RunConfigMeta
from findontime.connectors import ConnectorDocker, ConnectorParamiko
from findontime.drones import (
    InsafluFileProcessThread,
    LockWithOwner,
    TelevirFileProcessThread,
    signal_handler,
)
from findontime.insaflu_uploads import (
    InsafluFileProcess,
    PreMainWithMetadata,
    TelevirFileProcess,
)
from findontime.upload_utils import (
    InsafluUploadRemote,
    UploadAll,
    UploadLast,
    UploadNone,
)


def get_arguments():
    parser = argparse.ArgumentParser(description="Process fastq files.")
    parser.add_argument("-i", "--in_dir", help="Input directory", required=True)
    parser.add_argument("-o", "--out_dir", help="Output directory", required=True)
    parser.add_argument(
        "-s",
        "--sleep",
        help="Sleep time between checks in monitor mode",
        default=600,
        type=int,
    )

    parser.add_argument(
        "-n",
        "--tag",
        help="name tag, if given, will be added to the output file names",
        required=False,
        type=str,
        default="some_tag",
    )

    parser.add_argument(
        "--config", help="config file", required=False, type=str, default="config.ini"
    )

    parser.add_argument(
        "--max_size",
        help="max size of the output file, in kilobytes",
        type=int,
        default=400000,
    )

    parser.add_argument("--merge", help="merge files", action="store_true")

    parser.add_argument("--downsize", help="downsize fastq files", action="store_true")

    parser.add_argument(
        "--upload",
        default="last",
        choices=["last", "all", "none"],
        help="file upload stategy (default: last)",
    )

    parser.add_argument(
        "--connect",
        default="docker",
        choices=["docker", "ssh"],
        help="file upload stategy (default: docker)",
    )

    parser.add_argument(
        "--keep_names", help="keep original file names", action="store_true"
    )

    parser.add_argument(
        "--monitor", help="monitor directory until killed", action="store_true"
    )

    parser.add_argument(
        "--televir",
        help="deploy televir pathogen identification on each sample",
        action="store_true",
    )

    return ArgsClass(**parser.parse_args().__dict__)


@dataclass
class ArgsClass:
    in_dir: str
    out_dir: str
    sleep: int
    tag: str
    config: str
    merge: bool
    downsize: bool
    upload: str
    connect: str
    keep_names: bool
    monitor: bool
    televir: bool
    max_size: int

    def __post_init__(self):
        self.in_dir = os.path.abspath(self.in_dir)
        self.out_dir = os.path.abspath(self.out_dir)
        self.config = os.path.abspath(self.config)
        self.max_size = self.max_size * 1000


class MainInsaflu:
    def __init__(self):
        pass

    def select_manager(self, args: ArgsClass):
        if args.upload == "none":
            print("No upload specified, using fastq handler")
            return FastqHandlerManager(args)
        else:
            return InsafluManager(args)

    def run(self):
        args = get_arguments()

        if args.merge is True and args.upload == "all":
            ### inform user of behaviour and ask if to continue
            print(
                "Merge and upload all will result in successive files being merged and uploaded, is this what you want?"
            )
            print("If not, please use --upload last or --upload none")
            print("If you want to continue, please press enter")
            user_input = input()
            if user_input != "":
                sys.exit(0)

        manager = self.select_manager(args)
        manager.run()


class Deployer(Protocol):
    def __init__(self, args: ArgsClass) -> None:
        ...

    def run(self) -> None:
        ...


class FastqHandlerManager:
    def __init__(self, args: ArgsClass):
        self.args = args

    def setup_config(self, args: ArgsClass):
        actions = []

        if args.merge:
            actions.append(ProcessActionMergeWithLast())

        if args.downsize:
            actions.append(ProcessActionDownsize(args.max_size))

        if not actions:
            print("No actions specified, will merge files by default")
            actions.append(ProcessActionMergeWithLast())

        run_metadata = RunConfigMeta(
            fastq_dir=args.in_dir,
            output_dir=args.out_dir,
            name_tag=args.tag,
            actions=actions,
            keep_name=args.keep_names,
            sleep_time=args.sleep,
            max_size=(args.max_size),
        )
        actions = []

        return run_metadata

    def setup_compressor(self, run_metadata: RunConfigMeta):
        compressor = PreMainWithMetadata(run_metadata)

        return compressor

    def run(self):
        compressor = self.setup_compressor(self.setup_config(self.args))

        if self.args.monitor:
            compressor.run_until_killed()

        else:
            compressor.run()


class InsafluManager:
    def __init__(self, args: ArgsClass):
        self.args = args

    def setup_config(self, args: ArgsClass, test=False):
        # check input dir exists
        if not os.path.isdir(args.in_dir):
            print("Input directory does not exist")
            sys.exit(1)

        # check config file exists
        if not os.path.isfile(args.config):
            print("Config file does not exist")
            sys.exit(1)

        # create connector
        if args.connect == "docker":
            connector = ConnectorDocker(args.config)
            if test:
                connector.set_interactive(False)

        else:
            connector = ConnectorParamiko(args.config)

        insaflu_upload = InsafluUploadRemote(connector, args.config)

        # determine upload strategy
        if args.upload == "last":
            upload_strategy = UploadLast
        elif args.upload == "none":
            upload_strategy = UploadNone
        else:
            upload_strategy = UploadAll

        # determine actions
        actions = []
        if args.merge:
            actions.append(ProcessActionMergeWithLast())

        if args.downsize:
            actions.append(ProcessActionDownsize(args.max_size))

        if not actions:
            print("No actions specified, will merge files by default")
            actions.append(ProcessActionMergeWithLast())

        # create run metadata

        run_metadata = InfluConfig(
            fastq_dir=args.in_dir,
            output_dir=args.out_dir,
            name_tag=args.tag,
            uploader=insaflu_upload,
            upload_strategy=upload_strategy,
            actions=actions,
            max_size=args.max_size,
            keep_name=args.keep_names,
            sleep_time=args.sleep,
            deploy_televir=args.televir,
            monitor=args.monitor,
        )

        return run_metadata

    def generate_runners(
        self, run_metadata: InfluConfig
    ) -> Tuple[InsafluFileProcess, TelevirFileProcess]:
        influ_compressor = InsafluFileProcess(
            run_metadata,
        )

        televir_processor = TelevirFileProcess(
            run_metadata,
        )

        return influ_compressor, televir_processor

    def generate_threads(
        self, file_processor: InsafluFileProcess, televir_processor: TelevirFileProcess
    ) -> Tuple[InsafluFileProcessThread, TelevirFileProcessThread]:
        lock = LockWithOwner()
        lock.owner = "A"

        file_processor_task = InsafluFileProcessThread(file_processor, lock)
        televir_processor_task = TelevirFileProcessThread(televir_processor, lock)

        file_processor_task.daemon = True
        televir_processor_task.daemon = True

        return file_processor_task, televir_processor_task

    def monitor_tasks(
        self,
        file_processor_task: InsafluFileProcessThread,
        televir_processor_task: TelevirFileProcessThread,
        args: ArgsClass,
    ):
        if not args.monitor:
            if file_processor_task.counter > 0 and televir_processor_task.counter > 0:
                return False

        if not file_processor_task.is_alive() and not televir_processor_task.is_alive():
            return False

        if file_processor_task.error or televir_processor_task.error:
            return False

        return True

    def run(self):
        args = self.args

        run_metadata = self.setup_config(args)

        influ_compressor, televir_processor = self.generate_runners(run_metadata)

        file_processor_task, televir_processor_task = self.generate_threads(
            influ_compressor, televir_processor
        )

        signal.signal(signal.SIGINT, signal_handler)

        loop = True

        try:
            file_processor_task.start()
            televir_processor_task.start()

            while loop:
                new_loop = self.monitor_tasks(
                    file_processor_task, televir_processor_task, args
                )
                loop = new_loop

                time.sleep(2)

            print("Waiting for tasks to finish...")

        except Exception as e:
            print("Error in main thread.")
            print(e)

        finally:
            print("Stopping tasks...")

            file_processor_task.stop()
            televir_processor_task.stop()

            file_processor_task.join()
            televir_processor_task.join()

            print("Done!")

            sys.exit(0)
