"""Parsing helpers that fan out CLI arguments into raw job records."""

from __future__ import annotations

import json
from collections.abc import Sequence
from pathlib import Path

from ..._models_artifacts import (
    HtmlSnapshotRequest,
    MotionSnapshotFormat,
    MotionSnapshotRequest,
    ScreenshotArtifactsRequest,
)
from .schema import RawJobRecord, ScreenshotCliArgs
from .utils import derive_job_id

__all__ = ["load_cli_inputs"]


def _load_snippets(paths: Sequence[Path]) -> list[str]:
    snippets: list[str] = []
    for path in paths:
        snippets.append(path.read_text(encoding="utf-8"))
    return snippets


def _input_error(path: Path, line_number: int, message: str) -> ValueError:
    return ValueError(f"{path}:{line_number}: {message}")


def _optional_string(path: Path, line_number: int, field: str, value: object) -> str | None:
    if value is None:
        return None
    if not isinstance(value, str):
        raise _input_error(path, line_number, f"'{field}' must be a string")
    text = value.strip()
    return text or None


def _optional_bool(path: Path, line_number: int, field: str, value: object) -> bool | None:
    if value is None:
        return None
    if isinstance(value, bool):
        return value
    raise _input_error(path, line_number, f"'{field}' must be a boolean")


def _optional_float(path: Path, line_number: int, field: str, value: object) -> float | None:
    if value is None:
        return None
    if isinstance(value, bool):
        raise _input_error(path, line_number, f"'{field}' must be a number")
    if isinstance(value, int | float):
        return float(value)
    raise _input_error(path, line_number, f"'{field}' must be a number")


def _optional_int(path: Path, line_number: int, field: str, value: object) -> int | None:
    if value is None:
        return None
    if isinstance(value, bool) or not isinstance(value, int):
        raise _input_error(path, line_number, f"'{field}' must be an integer")
    return value


def _parse_motion_format(
    path: Path,
    line_number: int,
    raw_path: Path,
    value: object,
) -> MotionSnapshotFormat:
    if value is None:
        suffix = raw_path.suffix.lower()
        if suffix and suffix != ".gif":
            raise _input_error(
                path,
                line_number,
                "'artifacts.motion_snapshot.path' must end in '.gif' unless "
                "'artifacts.motion_snapshot.format' is provided",
            )
        return MotionSnapshotFormat.GIF
    if not isinstance(value, str):
        raise _input_error(path, line_number, "'artifacts.motion_snapshot.format' must be a string")
    normalized = value.strip().lower()
    if normalized != MotionSnapshotFormat.GIF.value:
        raise _input_error(
            path,
            line_number,
            f"'artifacts.motion_snapshot.format' must be one of: {MotionSnapshotFormat.GIF.value}",
        )
    return MotionSnapshotFormat.GIF


def _parse_artifacts(
    path: Path,
    line_number: int,
    record: dict[str, object],
) -> ScreenshotArtifactsRequest:
    if "html_snapshot_path" in record:
        raise _input_error(
            path,
            line_number,
            "'html_snapshot_path' is no longer supported; use "
            "'artifacts.html_snapshot.path' instead",
        )

    raw_artifacts = record.get("artifacts")
    if raw_artifacts is None:
        return ScreenshotArtifactsRequest()
    if not isinstance(raw_artifacts, dict):
        raise _input_error(path, line_number, "'artifacts' must be an object")

    html_snapshot: HtmlSnapshotRequest | None = None
    motion_snapshot: MotionSnapshotRequest | None = None

    raw_html = raw_artifacts.get("html_snapshot")
    if raw_html is not None:
        if not isinstance(raw_html, dict):
            raise _input_error(path, line_number, "'artifacts.html_snapshot' must be an object")
        raw_html_path = _optional_string(
            path,
            line_number,
            "artifacts.html_snapshot.path",
            raw_html.get("path"),
        )
        if raw_html_path is None:
            raise _input_error(
                path,
                line_number,
                "missing non-empty 'artifacts.html_snapshot.path'",
            )
        html_snapshot = HtmlSnapshotRequest(path=Path(raw_html_path))

    raw_motion = raw_artifacts.get("motion_snapshot")
    if raw_motion is not None:
        if not isinstance(raw_motion, dict):
            raise _input_error(path, line_number, "'artifacts.motion_snapshot' must be an object")
        raw_motion_path = _optional_string(
            path,
            line_number,
            "artifacts.motion_snapshot.path",
            raw_motion.get("path"),
        )
        if raw_motion_path is None:
            raise _input_error(
                path,
                line_number,
                "missing non-empty 'artifacts.motion_snapshot.path'",
            )
        motion_path = Path(raw_motion_path)
        duration_s = _optional_float(
            path,
            line_number,
            "artifacts.motion_snapshot.duration_s",
            raw_motion.get("duration_s"),
        )
        fps = _optional_int(
            path,
            line_number,
            "artifacts.motion_snapshot.fps",
            raw_motion.get("fps"),
        )
        motion_snapshot = MotionSnapshotRequest(
            path=motion_path,
            format=_parse_motion_format(
                path,
                line_number,
                motion_path,
                raw_motion.get("format"),
            ),
            duration_s=3.0 if duration_s is None else duration_s,
            fps=5 if fps is None else fps,
            preserve_motion=(
                bool(
                    _optional_bool(
                        path,
                        line_number,
                        "artifacts.motion_snapshot.preserve_motion",
                        raw_motion.get("preserve_motion"),
                    )
                )
                if "preserve_motion" in raw_motion
                else True
            ),
            required=(
                bool(
                    _optional_bool(
                        path,
                        line_number,
                        "artifacts.motion_snapshot.required",
                        raw_motion.get("required"),
                    )
                )
                if "required" in raw_motion
                else False
            ),
        )

    try:
        return ScreenshotArtifactsRequest(
            html_snapshot=html_snapshot,
            motion_snapshot=motion_snapshot,
        )
    except (TypeError, ValueError) as exc:
        raise _input_error(path, line_number, str(exc)) from exc


def load_cli_inputs(args: ScreenshotCliArgs) -> tuple[list[RawJobRecord], list[str], list[str]]:
    """Return raw job records plus pre-loaded CSS/JS snippets."""

    backend = (args.backend or "playwright").strip().lower()
    css_snippets = _load_snippets(args.extra_css_paths)
    js_snippets = _load_snippets(args.extra_js_paths)

    records: list[RawJobRecord] = []

    if args.input:
        with args.input.open("r", encoding="utf-8") as handle:
            for line_number, line in enumerate(handle, start=1):
                if not line.strip():
                    continue
                record = json.loads(line)
                if not isinstance(record, dict):
                    raise _input_error(args.input, line_number, "expected JSON object")

                url = _optional_string(args.input, line_number, "url", record.get("url"))
                if url is None:
                    raise _input_error(args.input, line_number, "missing non-empty 'url'")

                job_id = (
                    _optional_string(args.input, line_number, "job_id", record.get("job_id"))
                    or _optional_string(args.input, line_number, "site_id", record.get("site_id"))
                    or derive_job_id(url)
                )
                partition = (
                    _optional_string(
                        args.input,
                        line_number,
                        "partition_date",
                        record.get("partition_date"),
                    )
                    or args.partition_date
                )

                raw_options = record.get("options")
                if raw_options is None:
                    overrides = None
                elif isinstance(raw_options, dict):
                    overrides = raw_options
                else:
                    raise _input_error(args.input, line_number, "'options' must be an object")

                artifacts = _parse_artifacts(args.input, line_number, record)

                metadata = record.get("metadata")
                if metadata is None:
                    metadata_dict: dict[str, object] = {}
                elif isinstance(metadata, dict):
                    metadata_dict = dict(metadata)
                else:
                    raise _input_error(args.input, line_number, "'metadata' must be an object")
                metadata_dict.setdefault("source", "cli")
                records.append(
                    RawJobRecord(
                        job_id=job_id,
                        url=url,
                        backend=backend,
                        partition_date=partition,
                        artifacts=artifacts,
                        metadata=metadata_dict,
                        options_override=overrides,
                    )
                )

    urls = [url.strip() for url in (args.urls or []) if url and url.strip()]
    site_ids = [sid.strip() for sid in (args.site_ids or []) if sid and sid.strip()]
    if urls:
        for index, url in enumerate(urls):
            fallback = f"site-{index + 1}"
            job_id = (
                site_ids[index] if index < len(site_ids) else derive_job_id(url, fallback=fallback)
            )
            records.append(
                RawJobRecord(
                    job_id=job_id,
                    url=url,
                    backend=backend,
                    partition_date=args.partition_date,
                    artifacts=ScreenshotArtifactsRequest(),
                    metadata={"source": "cli", "input": "args"},
                    options_override=None,
                )
            )

    return records, css_snippets, js_snippets
