from io import BytesIO

from pydantic import BaseModel, Field
from typing_extensions import override

from comfy_api.latest import IO, ComfyExtension, Input, InputImpl
from comfy_api_nodes.util import (
    ApiEndpoint,
    download_url_to_video_output,
    get_number_of_images,
    poll_op,
    sync_op,
    sync_op_raw,
    upload_audio_to_comfyapi,
    upload_images_to_comfyapi,
    validate_string,
)

MODELS_MAP = {
    "LTX-2 (Pro)": "ltx-2-pro",
    "LTX-2 (Fast)": "ltx-2-fast",
}

V25_MODELS_MAP = {
    "LTX-2.5 (Fast)": "ltx-2-5-fast",
    "LTX-2.5 (Pro)": "ltx-2-5-pro",
}


class ExecuteTaskRequest(BaseModel):
    prompt: str = Field(...)
    model: str = Field(...)
    duration: int = Field(...)
    resolution: str = Field(...)
    fps: int | None = Field(25)
    generate_audio: bool | None = Field(True)
    image_uri: str | None = Field(None)
    last_frame_uri: str | None = Field(None)


class AudioToVideoRequest(BaseModel):
    prompt: str = Field(...)
    model: str = Field(...)
    resolution: str = Field(...)
    audio_uri: str = Field(...)
    image_uri: str | None = Field(None)


class Ltx25SubmitResponse(BaseModel):
    id: str = Field(...)


class Ltx25JobResult(BaseModel):
    video_url: str | None = Field(None)


class Ltx25JobStatusResponse(BaseModel):
    id: str = Field(...)
    status: str = Field(...)
    result: Ltx25JobResult | None = Field(None)


async def _v25_submit_and_poll(cls: type[IO.ComfyNode], route: str, data: BaseModel) -> IO.NodeOutput:
    submit = await sync_op(
        cls,
        ApiEndpoint(f"/proxy/ltx/v2/{route}", "POST"),
        response_model=Ltx25SubmitResponse,
        data=data,
        max_retries=1,
    )
    job = await poll_op(
        cls,
        ApiEndpoint(f"/proxy/ltx/v2/{route}/{submit.id}"),
        response_model=Ltx25JobStatusResponse,
        status_extractor=lambda r: r.status,
    )
    if not job.result or not job.result.video_url:
        raise RuntimeError(f"LTX job {job.id} completed without a video URL.")
    return IO.NodeOutput(await download_url_to_video_output(job.result.video_url, cls=cls))


PRICE_BADGE = IO.PriceBadge(
    depends_on=IO.PriceBadgeDepends(widgets=["model", "duration", "resolution"]),
    expr="""
    (
      $prices := {
        "ltx-2 (pro)": {"1920x1080":0.06,"2560x1440":0.12,"3840x2160":0.24},
        "ltx-2 (fast)": {"1920x1080":0.04,"2560x1440":0.08,"3840x2160":0.16}
      };
      $modelPrices := $lookup($prices, $lowercase(widgets.model));
      $pps := $lookup($modelPrices, widgets.resolution);
      {"type":"usd","usd": $pps * widgets.duration}
    )
    """,
)

V25_PRICE_BADGE = IO.PriceBadge(
    depends_on=IO.PriceBadgeDepends(widgets=["model", "model.duration", "model.resolution"]),
    expr="""
    (
      $prices := {
        "ltx-2.5 (fast)": {
          "1280x720":0.1287,"720x1280":0.1287,
          "1920x1080":0.1859,"1080x1920":0.1859,
          "2560x1440":0.2717,"1440x2560":0.2717,
          "3840x2160":0.429,"2160x3840":0.429
        },
        "ltx-2.5 (pro)": {
          "1280x720":0.1716,"720x1280":0.1716,
          "1920x1080":0.2431,"1080x1920":0.2431
        }
      };
      $model := $lookup(widgets, "model");
      $table := $type($model) = "string" ? $lookup($prices, $model) : undefined;
      $res := $lookup(widgets, "model.resolution");
      $pps := $type($table) = "object" and $type($res) = "string" ? $lookup($table, $res) : undefined;
      $durRaw := $lookup(widgets, "model.duration");
      $dur := $type($durRaw) in ["string", "number"] ? $number($durRaw) : undefined;
      $type($pps) = "number" and $type($dur) = "number"
        ? {"type":"usd","usd": $pps * $dur}
        : undefined
    )
    """,
)

V25_A2V_PRICE_BADGE = IO.PriceBadge(
    depends_on=IO.PriceBadgeDepends(widgets=["model"]),
    expr="""
    (
      $rates := {"ltx-2.5 (fast)":0.1859, "ltx-2.5 (pro)":0.2431};
      $model := $lookup(widgets, "model");
      $rate := $type($model) = "string" ? $lookup($rates, $model) : undefined;
      $type($rate) = "number"
        ? {"type":"usd","usd": $rate, "format":{"suffix":"/second"}}
        : undefined
    )
    """,
)


def _v25_generation_inputs(
    durations: list[str], resolutions: list[str], fps_options: list[str], tooltip: str | None
) -> list:
    return [
        IO.Combo.Input(
            "duration",
            options=durations,
            default="8",
            tooltip=tooltip,
        ),
        IO.Combo.Input(
            "resolution",
            options=resolutions,
            default="1920x1080",
        ),
        IO.Combo.Input("fps", options=fps_options, default="25"),
        IO.Boolean.Input(
            "generate_audio",
            default=True,
            tooltip="When true, the generated video will include AI-generated audio matching the scene.",
            advanced=True,
        ),
    ]


def _v25_model_combo() -> IO.DynamicCombo.Input:
    return IO.DynamicCombo.Input(
        "model",
        options=[
            IO.DynamicCombo.Option(
                "LTX-2.5 (Fast)",
                _v25_generation_inputs(
                    ["2", "3", "4", "5", "6", "8", "10", "12", "14", "16", "18", "20"],
                    [
                        "1280x720",
                        "720x1280",
                        "1920x1080",
                        "1080x1920",
                        "2560x1440",
                        "1440x2560",
                        "3840x2160",
                        "2160x3840",
                    ],
                    ["24", "25", "48", "50"],
                    "Video duration in seconds. Durations over 10s require a 720p/1080p resolution and 24/25 FPS.",
                ),
            ),
            IO.DynamicCombo.Option(
                "LTX-2.5 (Pro)",
                _v25_generation_inputs(
                    ["2", "3", "4", "5", "6", "8", "10"],
                    ["1280x720", "720x1280", "1920x1080", "1080x1920"],
                    ["24", "25", "50"],
                    "Video duration in seconds.",
                ),
            ),
        ],
    )


def _v25_seed_input() -> IO.Int.Input:
    return IO.Int.Input(
        "seed",
        default=42,
        min=0,
        max=0xFFFFFFFF,
        control_after_generate=True,
        tooltip="Seed to determine if node should re-run; "
        "actual results are nondeterministic regardless of seed.",
    )


def _v25_validate_settings(model: dict) -> None:
    if int(model["duration"]) > 10 and (
        int(model["fps"]) > 25 or model["resolution"] in ("2560x1440", "1440x2560", "3840x2160", "2160x3840")
    ):
        raise ValueError("Durations over 10s require a 720p or 1080p resolution and 24/25 FPS.")


class TextToVideoNode(IO.ComfyNode):
    @classmethod
    def define_schema(cls):
        return IO.Schema(
            node_id="LtxvApiTextToVideo",
            display_name="LTXV Text To Video",
            category="partner/video/LTXV",
            description="Professional-quality videos with customizable duration and resolution.",
            inputs=[
                IO.Combo.Input("model", options=list(MODELS_MAP.keys())),
                IO.String.Input(
                    "prompt",
                    multiline=True,
                    default="",
                ),
                IO.Combo.Input("duration", options=[6, 8, 10, 12, 14, 16, 18, 20], default=8),
                IO.Combo.Input(
                    "resolution",
                    options=[
                        "1920x1080",
                        "2560x1440",
                        "3840x2160",
                    ],
                ),
                IO.Combo.Input("fps", options=[25, 50], default=25),
                IO.Boolean.Input(
                    "generate_audio",
                    default=False,
                    optional=True,
                    tooltip="When true, the generated video will include AI-generated audio matching the scene.",
                    advanced=True,
                ),
            ],
            outputs=[
                IO.Video.Output(),
            ],
            hidden=[
                IO.Hidden.auth_token_comfy_org,
                IO.Hidden.api_key_comfy_org,
                IO.Hidden.unique_id,
            ],
            is_api_node=True,
            is_deprecated=True,
            price_badge=PRICE_BADGE,
        )

    @classmethod
    async def execute(
        cls,
        model: str,
        prompt: str,
        duration: int,
        resolution: str,
        fps: int = 25,
        generate_audio: bool = False,
    ) -> IO.NodeOutput:
        validate_string(prompt, min_length=1, max_length=10000)
        if duration > 10 and (model != "LTX-2 (Fast)" or resolution != "1920x1080" or fps != 25):
            raise ValueError(
                "Durations over 10s are only available for the Fast model at 1920x1080 resolution and 25 FPS."
            )
        response = await sync_op_raw(
            cls,
            ApiEndpoint("/proxy/ltx/v1/text-to-video", "POST"),
            data=ExecuteTaskRequest(
                prompt=prompt,
                model=MODELS_MAP[model],
                duration=duration,
                resolution=resolution,
                fps=fps,
                generate_audio=generate_audio,
            ),
            as_binary=True,
            max_retries=1,
        )
        return IO.NodeOutput(InputImpl.VideoFromFile(BytesIO(response)))


class ImageToVideoNode(IO.ComfyNode):
    @classmethod
    def define_schema(cls):
        return IO.Schema(
            node_id="LtxvApiImageToVideo",
            display_name="LTXV Image To Video",
            category="partner/video/LTXV",
            description="Professional-quality videos with customizable duration and resolution based on start image.",
            inputs=[
                IO.Image.Input("image", tooltip="First frame to be used for the video."),
                IO.Combo.Input("model", options=list(MODELS_MAP.keys())),
                IO.String.Input(
                    "prompt",
                    multiline=True,
                    default="",
                ),
                IO.Combo.Input("duration", options=[6, 8, 10, 12, 14, 16, 18, 20], default=8),
                IO.Combo.Input(
                    "resolution",
                    options=[
                        "1920x1080",
                        "2560x1440",
                        "3840x2160",
                    ],
                ),
                IO.Combo.Input("fps", options=[25, 50], default=25),
                IO.Boolean.Input(
                    "generate_audio",
                    default=False,
                    optional=True,
                    tooltip="When true, the generated video will include AI-generated audio matching the scene.",
                    advanced=True,
                ),
            ],
            outputs=[
                IO.Video.Output(),
            ],
            hidden=[
                IO.Hidden.auth_token_comfy_org,
                IO.Hidden.api_key_comfy_org,
                IO.Hidden.unique_id,
            ],
            is_api_node=True,
            is_deprecated=True,
            price_badge=PRICE_BADGE,
        )

    @classmethod
    async def execute(
        cls,
        image: Input.Image,
        model: str,
        prompt: str,
        duration: int,
        resolution: str,
        fps: int = 25,
        generate_audio: bool = False,
    ) -> IO.NodeOutput:
        validate_string(prompt, min_length=1, max_length=10000)
        if duration > 10 and (model != "LTX-2 (Fast)" or resolution != "1920x1080" or fps != 25):
            raise ValueError(
                "Durations over 10s are only available for the Fast model at 1920x1080 resolution and 25 FPS."
            )
        if get_number_of_images(image) != 1:
            raise ValueError("Currently only one input image is supported.")
        response = await sync_op_raw(
            cls,
            ApiEndpoint("/proxy/ltx/v1/image-to-video", "POST"),
            data=ExecuteTaskRequest(
                image_uri=(await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png"))[0],
                prompt=prompt,
                model=MODELS_MAP[model],
                duration=duration,
                resolution=resolution,
                fps=fps,
                generate_audio=generate_audio,
            ),
            as_binary=True,
            max_retries=1,
        )
        return IO.NodeOutput(InputImpl.VideoFromFile(BytesIO(response)))


class Ltx25TextToVideoNode(IO.ComfyNode):
    @classmethod
    def define_schema(cls):
        return IO.Schema(
            node_id="LtxApi25TextToVideo",
            display_name="LTX 2.5 Text To Video",
            category="partner/video/LTXV",
            description="Professional-quality videos with customizable duration and resolution.",
            inputs=[
                _v25_model_combo(),
                IO.String.Input(
                    "prompt",
                    multiline=True,
                    default="",
                ),
                _v25_seed_input(),
            ],
            outputs=[
                IO.Video.Output(),
            ],
            hidden=[
                IO.Hidden.auth_token_comfy_org,
                IO.Hidden.api_key_comfy_org,
                IO.Hidden.unique_id,
            ],
            is_api_node=True,
            price_badge=V25_PRICE_BADGE,
        )

    @classmethod
    async def execute(
        cls,
        model: dict,
        prompt: str,
        seed: int = 42,
    ) -> IO.NodeOutput:
        validate_string(prompt, min_length=1, max_length=10000)
        _v25_validate_settings(model)
        return await _v25_submit_and_poll(
            cls,
            "text-to-video",
            ExecuteTaskRequest(
                prompt=prompt,
                model=V25_MODELS_MAP[model["model"]],
                duration=int(model["duration"]),
                resolution=model["resolution"],
                fps=int(model["fps"]),
                generate_audio=model["generate_audio"],
            ),
        )


class Ltx25ImageToVideoNode(IO.ComfyNode):
    @classmethod
    def define_schema(cls):
        return IO.Schema(
            node_id="LtxApi25ImageToVideo",
            display_name="LTX 2.5 Image To Video",
            category="partner/video/LTXV",
            description="Professional-quality videos with customizable duration and resolution based on start image.",
            inputs=[
                IO.Image.Input("image", tooltip="First frame to be used for the video."),
                _v25_model_combo(),
                IO.String.Input(
                    "prompt",
                    multiline=True,
                    default="",
                ),
                _v25_seed_input(),
                IO.Image.Input(
                    "last_frame",
                    optional=True,
                    tooltip="Last frame to be used for the video.",
                ),
            ],
            outputs=[
                IO.Video.Output(),
            ],
            hidden=[
                IO.Hidden.auth_token_comfy_org,
                IO.Hidden.api_key_comfy_org,
                IO.Hidden.unique_id,
            ],
            is_api_node=True,
            price_badge=V25_PRICE_BADGE,
        )

    @classmethod
    async def execute(
        cls,
        image: Input.Image,
        model: dict,
        prompt: str,
        seed: int = 42,
        last_frame: Input.Image | None = None,
    ) -> IO.NodeOutput:
        validate_string(prompt, min_length=1, max_length=10000)
        _v25_validate_settings(model)
        if get_number_of_images(image) != 1:
            raise ValueError("Currently only one input image is supported.")
        last_frame_uri = None
        if last_frame is not None:
            if get_number_of_images(last_frame) != 1:
                raise ValueError("Currently only one last frame image is supported.")
            last_frame_uri = (await upload_images_to_comfyapi(cls, last_frame, max_images=1, mime_type="image/png"))[0]
        return await _v25_submit_and_poll(
            cls,
            "image-to-video",
            ExecuteTaskRequest(
                image_uri=(await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png"))[0],
                last_frame_uri=last_frame_uri,
                prompt=prompt,
                model=V25_MODELS_MAP[model["model"]],
                duration=int(model["duration"]),
                resolution=model["resolution"],
                fps=int(model["fps"]),
                generate_audio=model["generate_audio"],
            ),
        )


class Ltx25AudioToVideoNode(IO.ComfyNode):
    @classmethod
    def define_schema(cls):
        return IO.Schema(
            node_id="LtxApi25AudioToVideo",
            display_name="LTX 2.5 Audio To Video",
            category="partner/video/LTXV",
            description="Generate a video driven by an audio track, with an optional first frame image.",
            inputs=[
                IO.Audio.Input(
                    "audio",
                    tooltip="Audio track driving the video. Its length (2-20 seconds) sets the video duration.",
                ),
                IO.DynamicCombo.Input(
                    "model",
                    options=[
                        IO.DynamicCombo.Option(
                            "LTX-2.5 (Fast)",
                            [IO.Combo.Input("resolution", options=["1920x1080", "1080x1920"])],
                        ),
                        IO.DynamicCombo.Option(
                            "LTX-2.5 (Pro)",
                            [IO.Combo.Input("resolution", options=["1920x1080", "1080x1920"])],
                        ),
                    ],
                ),
                IO.String.Input(
                    "prompt",
                    multiline=True,
                    default="",
                ),
                _v25_seed_input(),
                IO.Image.Input(
                    "image",
                    optional=True,
                    tooltip="Optional first frame to be used for the video.",
                ),
            ],
            outputs=[
                IO.Video.Output(),
            ],
            hidden=[
                IO.Hidden.auth_token_comfy_org,
                IO.Hidden.api_key_comfy_org,
                IO.Hidden.unique_id,
            ],
            is_api_node=True,
            price_badge=V25_A2V_PRICE_BADGE,
        )

    @classmethod
    async def execute(
        cls,
        audio: Input.Audio,
        model: dict,
        prompt: str,
        seed: int = 42,
        image: Input.Image | None = None,
    ) -> IO.NodeOutput:
        validate_string(prompt, min_length=1, max_length=10000)
        audio_duration = audio["waveform"].shape[-1] / audio["sample_rate"]
        if not 2 <= audio_duration <= 20:
            raise ValueError(f"Audio duration must be between 2 and 20 seconds, got {audio_duration:.1f}s.")
        image_uri = None
        if image is not None:
            if get_number_of_images(image) != 1:
                raise ValueError("Currently only one input image is supported.")
            image_uri = (await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png"))[0]
        return await _v25_submit_and_poll(
            cls,
            "audio-to-video",
            AudioToVideoRequest(
                prompt=prompt,
                model=V25_MODELS_MAP[model["model"]],
                resolution=model["resolution"],
                audio_uri=await upload_audio_to_comfyapi(cls, audio),
                image_uri=image_uri,
            ),
        )


class LtxvApiExtension(ComfyExtension):
    @override
    async def get_node_list(self) -> list[type[IO.ComfyNode]]:
        return [
            TextToVideoNode,
            ImageToVideoNode,
            Ltx25TextToVideoNode,
            Ltx25ImageToVideoNode,
            Ltx25AudioToVideoNode,
        ]


async def comfy_entrypoint() -> LtxvApiExtension:
    return LtxvApiExtension()
