from typing import List


def prepare_and_render(
        p_input_video: str = "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        p_target_language: str = "",
        p_source_language: str = "",
        p_download_directory: str = "downloads",
        p_synthesis_directory: str = "synthesis",
        p_extract: bool = False,
        p_voices: List[str] = None,
        p_engines: List[str] = ["coqui"],
        p_output_video: str = "final_cut.mp4",
        p_clean_audio: bool = False,
        p_limit_start_time: str = None,
        p_limit_end_time: str = None,
        p_analysis: bool = False,
        p_speaker_number: str = "",
        p_num_speakers: int = 0,
        p_min_speakers: int = 0,
        p_max_speakers: int = 0,
        p_time_files: List[str] = [],
        p_prompt: str = None,
        p_debug: bool = False,
        p_prepare: bool = False,
        p_render: str = None,
        p_use_faster_whisper: bool = False,
        p_model: str = "large-v2"
        ):
    """
    Video Processing Workflow covering downloading, audio extraction,
    transcription, synthesis, and video combination.

    Downloads a YouTube video, transcribes its audio, and replaces
    the original voice with a synthetic one.

    Parameters:
    p_input_video (str): Path to local video or URL or ID of a YouTube video.
    p_target_language (str): Target language code for synthesis translation.
    p_source_language (str): Source language code for transcription.
        Automatically detected. Can be set manually in case that fails.
    p_download_directory (str): Directory to save downloaded files.
    p_synthesis_directory (str): Directory to save synthesized audio files.
    p_extract (bool): Whether to enable extraction of audio from the
        video file (and perform download instead). Can result in lower
        quality but has lower file sizes.
    p_voices (List[str]): Voice or voices used for synthesis.
    p_output_video (str): Filename for the output video with synthetic voice.
    p_clean_audio (bool): No preserve of original audio in the final video.
        Returns clean synthesis.
    p_limit_start_time (str): Time to start processing the video from.
    p_limit_end_time (str): Time to stop processing the video at.
    p_analysis (bool): Prints analysis of the video.
    p_speaker_number (str): Speaker number to be turned.
    p_num_speakers (int): Helps diarization. Specify the exact number of
        speakers in the video if you know it in advance.
    p_min_speakers (int): Helps diarization. Specify the minimum number of
        speakers in the video if you know it in advance.
    p_max_speakers (int): Helps diarization. Specify the maximum number of
        speakers in the video if you know it in advance.
    p_time_files (List[str]): Define timestamp file(s) for processing
        (basically performs multiple --from/--to)
    p_prompt (str): Text prompt for video processing instructions.
        For example, "speak like a pirate".
    p_debug (bool): Prints extended debugging output.
    p_prepare (bool): Generates full script with speaker analysis, sentence
        prompt transformation and translation. Can be continued.
    p_render (str): Renders a prepared full script.
    p_use_faster_whisper (bool): Usage of faster_whisper for transcription.
    p_model (str): Model used for transcription.
    """
    import time
    t_start = time.time()
    synthesis = None

    from .cli import verify_install
    if not verify_install("spleeter", "rubberband"):
        print("Spleeter is not installed. Please install it to proceed with audio splitting.")
        print("Spleeter is an audio separation tool developed by Deezer that allows you to split music tracks into different components such as vocals and accompaniment.")
        print("To install spleeter, you can run the following command:")
        print("\n   pip install spleeter\n")
        print("For more details and installation options, visit the official repository at: https://github.com/deezer/spleeter")
        return


    from .processing import ensure_directories
    ensure_directories([p_download_directory, p_synthesis_directory])

    if not p_model:
        p_model = "large-v2"

    print("input parameters: \n"
          f"- video: {p_input_video}\n"
          f"- translation language: {p_target_language}\n"
          f"- input video language: {p_source_language}\n"
          f"- download directory: {p_download_directory}\n"
          f"- synthesis directory: {p_synthesis_directory}\n"
          f"- extract: {p_extract}\n"
          f"- voices: {p_voices}\n"
          f"- output video: {p_output_video}\n"
          f"- clean audio: {p_clean_audio}\n"
          f"- start: {p_limit_start_time}\n"
          f"- end: {p_limit_end_time}\n"
          f"- analysis: {p_analysis}\n"
          f"- speaker number: {p_speaker_number}\n"
          f"- num speakers: {p_num_speakers}\n"
          f"- min speakers: {p_min_speakers}\n"
          f"- max speakers: {p_max_speakers}\n"
          f"- time files: {p_time_files}\n"
          f"- prompt: {p_prompt}"
          f"- debug: {p_debug}\n"
          f"- prepare: {p_prepare}\n"
          f"- render: {p_render}\n"
          f"- use faster: {p_use_faster_whisper}\n"
          f"- model: {p_model}\n"
          )

    # Download video (if no local video provided)
    # Extract audio from video
    if not p_render:
        from .processing import get_audio_and_muted_video
        audio_file, video_file_muted = get_audio_and_muted_video(
            p_input_video,
            p_download_directory,
            p_extract
        )

        from .processing import process_filename
        audio_file = process_filename(audio_file)

        from moviepy.editor import AudioFileClip
        with AudioFileClip(audio_file) as audio_clip:
            duration = audio_clip.duration

        from os.path import basename, join, splitext
        audio_file_name, _ = splitext(basename(audio_file))
        download_sub_directory = join(p_download_directory, audio_file_name)
        ensure_directories([download_sub_directory])

        # Determine processing start and end times
        # Consider time files if provided
        from .processing import get_processing_times
        limit_times, processing_start, processing_end = get_processing_times(
            p_time_files,
            download_sub_directory,
            p_limit_start_time,
            p_limit_end_time,
            duration
        )

        # Split audio into vocals and accompaniment
        # if not clean audio requested
        if not p_clean_audio:

            print(f"[{(time.time() - t_start):.1f}s] "
                  "splitting audio..."
                  )
            from .cut import split_audio
            vocal_path, accompaniment_path = split_audio(
                audio_file,
                p_download_directory
            )

            # from .cut import normalize_audio
            # vocal_fname, vocal_ext = vocal_path_raw.split(".")
            # vocal_path = f"{vocal_fname}_nrm.{vocal_ext}"
            # normalize_audio(vocal_path_raw, vocal_path)

        print(f"[{(time.time() - t_start):.1f}s] "
              f"splitting finished, vocal path is {vocal_path}..."
              )

    if not p_analysis and not p_prepare:
        print(f"[{(time.time() - t_start):.1f}s] "
              "early start synthesis engine (grab vram)..."
              )

        from .synthesis import Synthesis
        synthesis = Synthesis(
            language=p_target_language,
            voices=p_voices,
            engine_names=p_engines
            )

    # Render a prepared full script if requested
    import json
    if p_render:

        with open(p_render, 'r', encoding='utf-8') as file:
            full_script = json.load(file)

        from .render import render_video
        render_video(
            full_script["sentences"],
            full_script["metadata"]["synthesis_language"],
            full_script["metadata"]["synthesis_directory"],
            full_script["metadata"]["clean_audio"],
            full_script["metadata"]["audio_file"],
            full_script["metadata"]["accompaniment_path"],
            full_script["metadata"]["video_file_muted"],
            full_script["metadata"]["output_video"],
            time.time(),
            synthesis
            )

        return

    # Transcribe audio to text
    transcription_audio = vocal_path
    print(f"[{(time.time() - t_start):.1f}s] "
          f"transcribing audio {transcription_audio} with "
          "faster_whisper " if p_use_faster_whisper else "stable_whisper "
          f"model {p_model}...",
          end="", flush=True
          )

    from .transcribe import transcribe
    transcribed_segments, transcription_info = transcribe(
        transcription_audio,
        language=p_source_language,
        model=p_model,
        use_faster=p_use_faster_whisper
        )

    # Determine synthesis and target language
    source_language = transcription_info.language

    # Extract words with precise timestamps from transcription
    from .processing import get_extracted_words
    words = get_extracted_words(
        transcribed_segments,
        "words.txt",
        download_sub_directory,
        t_start
        )

    if p_debug:
        print("Words:")
        for word in words:
            print(f"{word.start:.1f}s - {word.end:.1f}s: "
                  f"{word.text}  ", flush=True, end=""
                  )
        print()

    # Only keep all words within the time limits
    from .processing import filter_by_time_limits
    words = filter_by_time_limits(
        words,
        limit_times,
        "forgiving",
        0.2
        )

    if p_debug:
        print("Time filtered words:")
        for word in words:
            print(f"{word.start:.1f}s - {word.end:.1f}s: "
                  f"{word.text}  ", flush=True, end=""
                  )
        print()

    # Perform speaker detection (diarization)
    print(f"[{(time.time() - t_start):.1f}s] analyzing audio...")

    from .diarize import diarize
    speakers = diarize(
        vocal_path,
        p_num_speakers,
        p_min_speakers,
        p_max_speakers
        )

    from .diarize import filter_speakers
    speakers = filter_speakers(speakers, processing_start, processing_end)

    from .diarize import print_speakers, speaker_files_exist
    print_speakers(speakers)
    if not p_time_files or not speaker_files_exist(speakers):
        from .diarize import write_speaker_timefiles
        write_speaker_timefiles(speakers, download_sub_directory)
    if p_analysis:
        return

    # Filter words to a single speaker if requested
    from .processing import filter_by_speaker
    words = filter_by_speaker(
        p_speaker_number,
        words,
        speakers
        )

    if len(p_speaker_number) > 0 and p_debug:

        print("Speaker filtered words:")
        for word in words:

            print(f"{word.start:.1f}s - {word.end:.1f}s: "
                  f"{word.text}  ", flush=True, end=""
                  )
        print()

    if len(words) == 0:

        print(f"[{(time.time() - t_start):.1f}s] "
              "no words to be turned, aborting..."
              )
        if synthesis:
            synthesis.close()
        return

    print(f"[{(time.time() - t_start):.1f}s] "
          f"{len(words)} words found..."
          )

    # Generate synthesizable fragments from words
    # based on punctuation and gap size between words
    print(f"[{(time.time() - t_start):.1f}s] "
          "creating synthesizable fragments..."
          )

    from .fragtokenizer import get_segments

    sentence_fragments = get_segments(transcribed_segments)

    from .fragtokenizer import create_synthesizable_fragments
    from .fragtokenizer import start_full_sentence_characters

    full_sentences = create_synthesizable_fragments(
        words,
        break_characters=start_full_sentence_characters
    )

    # Assign each fragment to it's parent full sentence
    from .fragtokenizer import assign_fragments_to_sentences
    assign_fragments_to_sentences(sentence_fragments, full_sentences)

    for sentence in full_sentences:
        print(f'{sentence["text"]} ({sentence["start"]:.1f}s - '
              f'{sentence["end"]:.1f}s) contains '
              f'{len(sentence["sentence_frags"])} fragments: '
              )
        for sentence_frag in sentence["sentence_frags"]:
            print(f'    {sentence_frag["text"]} '
                  f'({sentence_frag["start"]:.1f}s '
                  f'- {sentence_frag["end"]:.1f}s)'
                  )

    # Performs prompt action (style transformation) if requested
    # this changes the tone of each sentence fragment
    # while being aware of the speaking duration and the
    # parent full sentence.
    if p_prompt:
        print(f"[{(time.time() - t_start):.1f}s] "
              f"transforming sentences, applying \"{p_prompt}\"..."
              )
        from .prompt import transform_sentences
        transform_sentences(full_sentences, p_prompt)

    for sentence in full_sentences:
        print(f'{sentence["text"]} ({sentence["start"]:.1f}s - '
              f'{sentence["end"]:.1f}s) contains '
              f'{len(sentence["sentence_frags"])} fragments: '
              )
        for sentence_frag in sentence["sentence_frags"]:
            print(f'    {sentence_frag["text"]} '
                  f'({sentence_frag["start"]:.1f}s '
                  f'- {sentence_frag["end"]:.1f}s)'
                  )

    # assign sentences to speakers based on best overlapping interval
    from .processing import assign_sentence_to_speakers
    assign_sentence_to_speakers(sentence_fragments, speakers)

    from .transcribe import unload_model
    unload_model(p_use_faster_whisper)

    from .translate import perform_translation
    perform_translation(
        sentence_fragments,
        source_language,
        p_target_language
    )

    # Determine and set synthesis language
    synthesis_language = (
        p_target_language if len(p_target_language) > 0 else source_language
    )

    print(f"[{(time.time() - t_start):.1f}s] "
          "input language detected from transcription: "
          f"{source_language}\n"
          f"language selected for synthesis: {synthesis_language}"
          )

    # transform pure youtube ID into url if needed
    input_video_path = p_input_video
    from .download import check_youtube, ensure_youtube_url
    if check_youtube(input_video_path):
        input_video_path = ensure_youtube_url(input_video_path)

    script_metadata = {
        "input_video": input_video_path,
        "download_sub_directory": download_sub_directory,
        "duration": duration,
        "target_language": p_target_language,
        "input_video_language": p_source_language,
        "synthesis_language": synthesis_language,
        "download_directory": p_download_directory,
        "synthesis_directory": p_synthesis_directory,
        "extract": p_extract,
        "voices": p_voices,
        "output_video": p_output_video,
        "clean_audio": p_clean_audio,
        "start": p_limit_start_time,
        "end": p_limit_end_time,
        "analysis": p_analysis,
        "speaker_number": p_speaker_number,
        "num_speakers": p_num_speakers,
        "min_speakers": p_min_speakers,
        "max_speakers": p_max_speakers,
        "time_files": p_time_files,
        "prompt": p_prompt,
        "render": p_render,
        "use_faster": p_use_faster_whisper,
        "audio_file": audio_file,
        "accompaniment_path": accompaniment_path,
        "video_file_muted": video_file_muted,
        "prepare_start": t_start,
    }
    full_script = {
        "metadata": script_metadata,
        "sentences": sentence_fragments
    }
    render_script_path = join(download_sub_directory, "render_script.txt")
    with open(render_script_path, 'w', encoding='utf-8') as file:
        json.dump(sentence_fragments, file, indent=4)
    full_script_path = join(download_sub_directory, "full_script.txt")
    with open(full_script_path, 'w', encoding='utf-8') as file:
        json.dump(full_script, file, indent=4)

    if p_prepare:
        print(f"[{(time.time() - t_start):.1f}s] "
              f"full script written to {full_script_path}\n"
              "preparation finished..."
              )
        return

    from .render import render_video
    render_video(
        sentence_fragments,
        synthesis_language,
        p_synthesis_directory,
        p_clean_audio,
        audio_file,
        accompaniment_path,
        video_file_muted,
        p_output_video,
        t_start,
        synthesis
    )
