from __future__ import annotations

import base64
from pathlib import Path
from typing import Any


# Builder defaults below intentionally bias toward the current pipeline's safer implementation posture,
# not every raw doc default. In particular, `mode='std'` remains the scaffold default to avoid silently
# increasing cost while Phase 2 is still synchronizing contracts. Raw Omni docs currently describe
# `mode='pro'` as the endpoint default, but that should be a deliberate caller choice, not an implicit one.
#
# Runtime asset policy:
# - our local assets are the source of truth
# - when a payload field supports inline image data, local files are converted to raw base64 at build time
# - preserve endpoint-specific wire shapes instead of flattening everything to one shared contract
#   * image2video -> `image` / `image_tail`
#   * omni -> `image_list[].image_url`


def _looks_like_http_url(value: str) -> bool:
    return value.startswith('http://') or value.startswith('https://')


def _looks_like_data_url(value: str) -> bool:
    return value.startswith('data:')


def _looks_like_file_url(value: str) -> bool:
    return value.startswith('file://')


def _coerce_inline_image_data(value: str) -> str:
    raw = value.strip()
    if not raw:
        return raw
    if _looks_like_http_url(raw):
        return raw
    if _looks_like_data_url(raw):
        _, _, encoded = raw.partition(',')
        return encoded or raw
    candidate = Path(raw[7:]) if _looks_like_file_url(raw) else Path(raw)
    if candidate.exists() and candidate.is_file():
        return base64.b64encode(candidate.read_bytes()).decode('ascii')
    return raw


def _normalize_reference_image_list(image_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
    normalized: list[dict[str, Any]] = []
    for item in image_list:
        updated = dict(item)
        if isinstance(updated.get('image_url'), str):
            updated['image_url'] = _coerce_inline_image_data(updated['image_url'])
        if isinstance(updated.get('image'), str):
            updated['image'] = _coerce_inline_image_data(updated['image'])
        normalized.append(updated)
    return normalized


def build_text2video_payload(*, prompt: str, model_name: str | None = None, duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9', multi_shot: bool = False, shot_type: str | None = None, multi_prompt: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    payload = {'prompt': prompt, 'duration': duration, 'mode': mode, 'aspect_ratio': aspect_ratio}
    if model_name is not None:
        payload['model_name'] = model_name
    if multi_shot:
        payload['multi_shot'] = True
        if shot_type:
            payload['shot_type'] = shot_type
        if multi_prompt:
            payload['multi_prompt'] = multi_prompt
    return payload


# Non-Omni video families are now allowed to bind the latest 3.0 base model explicitly
# when production testing or routing requires it. Keep model choice caller-explicit.

def build_image2video_payload(*, image: str, prompt: str, model_name: str | None = None, duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9', image_tail: str | None = None, negative_prompt: str | None = None, element_list: list[dict[str, Any]] | None = None, voice_list: list[dict[str, Any]] | None = None, sound: str | None = None, cfg_scale: float | None = None, static_mask: Any | None = None, dynamic_masks: list[dict[str, Any]] | None = None, camera_control: dict[str, Any] | None = None, watermark_info: dict[str, Any] | None = None, multi_shot: bool = False, shot_type: str | None = None, multi_prompt: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    payload = {'image': _coerce_inline_image_data(image), 'prompt': prompt, 'duration': duration, 'mode': mode, 'aspect_ratio': aspect_ratio}
    if model_name is not None:
        payload['model_name'] = model_name
    if image_tail is not None:
        payload['image_tail'] = _coerce_inline_image_data(image_tail)
    if negative_prompt is not None:
        payload['negative_prompt'] = negative_prompt
    if element_list:
        payload['element_list'] = element_list
    if voice_list:
        payload['voice_list'] = voice_list
    if sound is not None:
        payload['sound'] = sound
    if cfg_scale is not None:
        payload['cfg_scale'] = cfg_scale
    if static_mask is not None:
        payload['static_mask'] = static_mask
    if dynamic_masks:
        payload['dynamic_masks'] = dynamic_masks
    if camera_control is not None:
        payload['camera_control'] = camera_control
    if watermark_info is not None:
        payload['watermark_info'] = watermark_info
    if multi_shot:
        payload['multi_shot'] = True
        if shot_type:
            payload['shot_type'] = shot_type
        if multi_prompt:
            payload['multi_prompt'] = multi_prompt
    return payload


# Omni-first safe sync:
# - `image_list[].image_url + type` is the strongest current image-reference path
# - `video_list[].video_url=<remote url>` is the leading scene-to-scene reference path
# - omit optional clusters unless the caller intentionally opts in

def build_omni_payload(*, prompt: str, model_name: str = 'kling-v3-omni', duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9', image_list: list[dict[str, Any]] | None = None, video_list: list[dict[str, Any]] | None = None, element_list: list[dict[str, Any]] | None = None, sound: str | None = None, multi_shot: bool = False, shot_type: str | None = None, multi_prompt: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    payload = {'model_name': model_name, 'prompt': prompt, 'duration': duration, 'mode': mode, 'aspect_ratio': aspect_ratio}
    if image_list:
        payload['image_list'] = _normalize_reference_image_list(image_list)
    if video_list:
        payload['video_list'] = video_list
    if element_list:
        payload['element_list'] = element_list
    if sound is not None:
        payload['sound'] = sound
    if multi_shot:
        payload['multi_shot'] = True
        if shot_type:
            payload['shot_type'] = shot_type
        if multi_prompt:
            payload['multi_prompt'] = multi_prompt
    return payload


def build_reference2video_payload(*, image_list: list[dict[str, Any]], prompt: str, model_name: str | None = None, duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9') -> dict[str, Any]:
    payload = {'image_list': _normalize_reference_image_list(image_list), 'prompt': prompt, 'duration': duration, 'mode': mode, 'aspect_ratio': aspect_ratio}
    if model_name is not None:
        payload['model_name'] = model_name
    return payload


def build_extend_payload(*, video_id: str | None = None, video_url: str | None = None, prompt: str = '', negative_prompt: str = '') -> dict[str, Any]:
    payload = {'prompt': prompt, 'negative_prompt': negative_prompt}
    if video_id:
        payload['video_id'] = video_id
    if video_url:
        payload['video_url'] = video_url
    return payload


def build_omni_first_frame_payload(*, prompt: str, image_url: str, model_name: str = 'kling-v3-omni', duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9') -> dict[str, Any]:
    return {
        'model_name': model_name,
        'prompt': prompt,
        'duration': duration,
        'mode': mode,
        'aspect_ratio': aspect_ratio,
        'image_list': [
            {
                'image_url': _coerce_inline_image_data(image_url),
                'type': 'first_frame',
            }
        ],
    }


def build_omni_video_reference_payload(*, prompt: str, video_url: str, refer_type: str = 'base', keep_original_sound: str = 'yes', model_name: str = 'kling-v3-omni', duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9', sound: str = 'off') -> dict[str, Any]:
    return {
        'model_name': model_name,
        'prompt': prompt,
        'duration': duration,
        'mode': mode,
        'aspect_ratio': aspect_ratio,
        # Current docs are strong enough to encode this as the helper default for video-reference Omni.
        # This does not claim anything broader about all audio semantics.
        'sound': sound,
        'video_list': [
            {
                'video_url': video_url,
                'refer_type': refer_type,
                'keep_original_sound': keep_original_sound,
            }
        ],
    }


def build_omni_element_payload(*, prompt: str, element_ids: list[int], model_name: str = 'kling-v3-omni', duration: str = '5', mode: str = 'std', aspect_ratio: str = '16:9') -> dict[str, Any]:
    return {
        'model_name': model_name,
        'prompt': prompt,
        'duration': duration,
        'mode': mode,
        'aspect_ratio': aspect_ratio,
        'element_list': [
            {'element_id': eid} for eid in element_ids
        ],
    }
