# core/processor.py

import logging
import os
import time

from moviepy import VideoFileClip, concatenate_videoclips

from rcdl.utils import echo, echoc, Progress
from .config import Config


def rm_file(f):
    try:
        os.remove(f)
        logging.info(f"removed file {f}")
    except Exception as e:
        logging.error(f"Failed to delete {f} due to: {e}")


def rename_moved_file(f):
    dir_name = os.path.dirname(f)
    file_name = os.path.basename(f)

    new_file_name = f".mv_{file_name}"
    new_path = os.path.join(dir_name, new_file_name)
    try:
        os.rename(f, new_path)
        logging.info(f"renamed file {f}->{new_path}")
    except Exception as e:
        logging.error(f"Failed to rename {f}->{new_path} due to {e}")


def fuse_csv(
    filename: str,
    threads: int,
    preset: str,
    number_of_parts: int,
    original_total_byte_size: int,
    result_byte_size: int,
    encoding_time: float,
):
    """Write info to fuse.csv for data gathering"""

    headers = [
        "filename",
        "threads",
        "preset",
        "number_of_parts",
        "original_total_byte_size",
        "result_byte_size",
        "encoding_time",
    ]
    headers_str = ""
    for h in headers:
        headers_str += h + ";"

    path = Config.FUSE_CSV_FILE
    if not os.path.exists(path):
        with open(path, "w") as f:
            f.write(headers_str + "\n")

    line = f"{filename};{threads};{preset};{number_of_parts};{original_total_byte_size};{result_byte_size};{encoding_time};\n"
    with open(path, "a") as f:
        f.write(line)


def find_all_parts_video(folder_path: str) -> list:
    """Find all video that are _p1, _p2 etc.., extract the title from [0 to _pX.mP4(not included)] and return a set"""
    groups = {}

    videos_files = os.listdir(folder_path)
    for video in videos_files:
        if video.startswith(".mv_"):
            continue
        if not video.endswith(".mp4"):
            continue

        name = video[:-4]  # remove .mp4
        if "_p" not in name:
            continue

        title, part = name.rsplit("_p", 1)
        if part.isdigit():
            part_num = int(part)
            groups.setdefault(title, []).append((part_num, video))

    result = []
    for title, files in groups.items():
        if len(files) <= 1:
            continue

        sorted_files = sorted(files, key=lambda x: x[0])

        filenames = [fname for _, fname in sorted_files]

        result.append(filenames)

    return result


def verify_parts_videos(groups: list):
    """Take a list of lists of parts videos, verify none are missing"""
    valid_groups = []

    for files in groups:
        part_numbers = []
        for f in files:
            name = f[:-4]  # remove .mp4

            if "_p" not in name:
                error_msg = (
                    f"Files {files} not valid due to file {f} missing '_p' in filename"
                )
                echoc(error_msg, "red")
                logging.error(error_msg)
                break  # invalid format

            _, part = name.rsplit("_p", 1)
            if not part.isdigit():
                error_msg = f"Files {files} not valid due to file {f} having a invalid part number (not a number)"
                echoc(error_msg, "red")
                logging.error(error_msg)
                break

            part_numbers.append(int(part))

        if not part_numbers:
            error_msg = f"Files {files} not valid due to empty part numbers"
            echoc(error_msg, "red")
            logging.error(error_msg)
            continue  # skip invalid/empty groups

        part_numbers.sort()
        min_p, max_p = 0, part_numbers[-1]
        expected = list(range(min_p, max_p + 1))
        if part_numbers == expected and min_p == 0:
            valid_groups.append(files)
        else:
            missing_parts = set(expected) - set(part_numbers)
            error_msg = f"Files {files} not valid due to a mismatch on the number of part: expected {len(expected)}, got {len(part_numbers)}\nMissing part: _p{missing_parts}"
            echoc(error_msg, "red")
            logging.error(error_msg)

    return valid_groups


def fuse_videos(groups: list, creator_folder_path: str):
    index = 0
    progress = Progress(len(groups))

    for videos_files in groups:
        # echo(f"[cdl] {progress.get_str()}")
        # get full absolute path
        for i in range(len(videos_files)):
            videos_files[i] = creator_folder_path + videos_files[i]

        # extract title for output
        filepath = str(videos_files[0])
        filepath = filepath[:-4]  # remove .mp4
        filepath_part = filepath.rsplit("_p", 1)
        filepath = filepath_part[0]
        filepath += ".mp4"

        # output filepath
        output_path = filepath
        if Config.DEBUG:
            output_path = f"dev/output_{index}.mp4"

        clips = []
        final = None

        try:
            # add clips
            for f in videos_files:
                clip = VideoFileClip(f)
                clips.append(clip)

            # concatenate them indepened of codec, sizes, resolution
            final = concatenate_videoclips(
                clips, method="compose"
            )  # "compose" handles different sizes/codecs

            # select number of threads
            threads = os.cpu_count()
            if threads is None:
                threads = 2

            # load preset from settings
            valid_preset = [
                "ultrafast",
                "superfast",
                "veryfast",
                "faster",
                "fast",
                "medium",
                "slow",
                "slower",
                "veryslow",
                "placebo",
            ]
            preset = Config.load_settings()["preset"]
            if preset not in valid_preset:
                error_msg = f"Preset {preset} is not valid. Valid preset are: {valid_preset}. Please check settings.json"
                logging.error(error_msg)
                echoc(error_msg, "red")
                return

            # logging info var
            parts_number = len(videos_files)
            total_size = 0  # in bytes
            for f in videos_files:
                total_size += os.path.getsize(f)
            start_time = time.time()

            msg = f"Concatenating {filepath} with {threads} threads"
            echo(msg)
            logging.info(msg)

            final.write_videofile(output_path, preset=preset, threads=threads)

            # log info data
            result_size = os.path.getsize(output_path)
            encoding_time = time.time() - start_time
            fuse_csv(
                filepath,
                threads,
                preset,
                parts_number,
                total_size,
                result_size,
                encoding_time,
            )

            msg = f"Result for {filepath}: threads: {threads}, preset: {preset}, nb_of_parts: {parts_number}, original_byte_size: {total_size}, result_size: {result_size}, time: {encoding_time}s"
            if Config.DEBUG:
                msg += f" --DEBUG: output_path != filepath: {output_path}"
            logging.info(msg)
            echo(msg)

        except Exception as e:
            error_msg = f"Failed to concatenate {filepath} due to e: {e}"
            echoc(error_msg, "red")
            logging.error(error_msg)
        else:
            # rm part file
            for f in videos_files:
                # rm_file(f)
                rename_moved_file(f)
        finally:
            # free memory
            for clip in clips:
                try:
                    clip.close()
                except Exception as e:
                    logging.warning(f"Failed to close clip: {e}")
            try:
                if final is not None:
                    final.close()
            except Exception as e:
                logging.warning(f"Failed to close VideoClip: {e}")

        index += 1
        progress.update()


def fuse_all_videos(creators: list):
    for creator in creators:
        creator_id = creator["creator_id"]
        creator_folder_path = str(Config.creator_folder(creator_id))

        echo(f"Finding part videos for {creator_id} in {creator_folder_path}")

        # find all part video
        groups = find_all_parts_video(creator_folder_path)
        msg = f"Find {len(groups)} video to fuse for creator {creator_id}"
        echo(msg)
        logging.info(msg)

        # validation of groups of part videos
        valid_groups = verify_parts_videos(groups)
        msg = f"Find {len(valid_groups)} valid groups to fuse; {len(groups) - len(valid_groups)} unvalid groups removed."
        echo(msg)
        logging.info(msg)

        # fuse vid
        if not Config.DRY_RUN:
            fuse_videos(valid_groups, creator_folder_path)
        else:
            echoc("DRY-RUN -> FUSE SKIPPED", "blue")
