"""Metadata fetching pipeline: fetch all pages, write sidecar JSON files."""

from __future__ import annotations

import json
from pathlib import Path

import aiofiles
import structlog

from xc_dl.api.client import XenoCantoClient
from xc_dl.api.models import APIResponse, Recording
from xc_dl.api.query import query_hash
from xc_dl.core.manifest import Manifest, ManifestEntry
from xc_dl.core.progress import MetadataFetchState
from xc_dl.core.taxonomy import lookup_family
from xc_dl.utils import (
    build_filename,
    build_relative_dir,
    normalise_url,
    original_ext_from_filename,
    parse_duration,
)

logger = structlog.get_logger()


def _recording_to_manifest_entry(rec: Recording, family: str, data_dir: Path) -> ManifestEntry:
    """Convert an API Recording to a ManifestEntry."""
    ext = original_ext_from_filename(rec.file_name)
    rel_dir = build_relative_dir(family, rec.gen, rec.sp)
    base_name_no_ext = build_filename(rec.id, rec.gen, rec.sp, "").rstrip(".")

    original_path = str(Path("original_recordings") / rel_dir / build_filename(rec.id, rec.gen, rec.sp, ext))
    metadata_path = str(Path("metadata") / rel_dir / f"{base_name_no_ext}.json")

    lat = None
    if rec.lat is not None:
        try:
            lat = float(rec.lat)
        except (ValueError, TypeError):
            pass
    lon = None
    if rec.lon is not None:
        try:
            lon = float(rec.lon)
        except (ValueError, TypeError):
            pass

    lic = normalise_url(rec.lic) if rec.lic else ""
    # Normalise license URL to a short label
    license_label = lic
    if "creativecommons.org" in lic:
        # Extract e.g. "CC-BY-NC-SA-4.0" from URL
        parts = lic.rstrip("/").split("/")
        if len(parts) >= 2:
            license_label = f"CC-{parts[-2].upper()}-{parts[-1]}"

    return ManifestEntry(
        xc_id=rec.id,
        gen=rec.gen,
        sp=rec.sp,
        ssp=rec.ssp,
        en=rec.en,
        family=family,
        group=rec.grp,
        country=rec.cnt,
        locality=rec.loc,
        lat=lat,
        lon=lon,
        type=rec.type_list,
        also=rec.also,
        quality=rec.q,
        length_seconds=parse_duration(rec.length),
        date=rec.date,
        recordist=rec.rec,
        license=license_label,
        original_format=ext,
        original_filename=rec.file_name,
        download_url=rec.file,
        original_path=original_path,
        metadata_path=metadata_path,
    )


async def _write_sidecar(rec: Recording, family: str, data_dir: Path) -> None:
    """Write a sidecar JSON file for a single recording."""
    rel_dir = build_relative_dir(family, rec.gen, rec.sp)
    base_name = build_filename(rec.id, rec.gen, rec.sp, "json")
    sidecar_path = data_dir / "metadata" / rel_dir / base_name

    sidecar_path.parent.mkdir(parents=True, exist_ok=True)

    ext = original_ext_from_filename(rec.file_name)
    original_path = str(Path("original_recordings") / rel_dir / build_filename(rec.id, rec.gen, rec.sp, ext))

    sidecar = {
        "xc_raw": rec.model_dump(by_alias=True),
        "xc_dl": {
            "original_path": original_path,
            "resampled_path": "",
            "sha256_original": "",
            "sha256_resampled": "",
            "downloaded_at": "",
            "original_format": ext,
            "converted_to": "",
            "conversion_params": {},
        },
    }

    async with aiofiles.open(sidecar_path, "w") as f:
        await f.write(json.dumps(sidecar, indent=2, ensure_ascii=False))


async def fetch_and_store_metadata(
    client: XenoCantoClient,
    query: str,
    data_dir: Path,
    per_page: int = 500,
    progress_callback: object = None,
) -> list[ManifestEntry]:
    """Fetch all metadata pages and write sidecars + manifest entries.

    Args:
        client: The API client.
        query: Xeno-canto query string.
        data_dir: Dataset root directory.
        per_page: Records per API page.
        progress_callback: Optional async callable(fetched, total) for progress updates.

    Returns:
        List of ManifestEntry objects for all recordings.
    """
    progress_dir = data_dir / ".progress"
    progress_dir.mkdir(parents=True, exist_ok=True)

    qhash = query_hash(query)
    fetch_state = MetadataFetchState(progress_dir)
    fetched_pages = fetch_state.get_fetched_pages(qhash)

    manifest = Manifest(data_dir)
    existing = manifest.read_all()

    # If all pages were already fetched, return cached manifest data directly
    if fetch_state.is_complete(qhash) and existing:
        logger.info("metadata_cached", total=len(existing))
        if progress_callback:
            await progress_callback(1, 1)
        return list(existing.values())

    entries: list[ManifestEntry] = []
    new_entries: list[ManifestEntry] = []
    total_pages: int | None = None

    async def on_page(page: int, response: APIResponse) -> None:
        nonlocal total_pages
        total_pages = response.numPages
        fetch_state.mark_page(qhash, page, response.numPages)
        if progress_callback:
            fetched = len(fetch_state.get_fetched_pages(qhash))
            await progress_callback(fetched, response.numPages)

    async for rec in client.fetch_all_pages(
        query, per_page=per_page, fetched_pages=fetched_pages, on_page=on_page
    ):
        family = lookup_family(rec.gen, data_dir)
        entry = _recording_to_manifest_entry(rec, family, data_dir)
        entries.append(entry)

        if entry.xc_id not in existing:
            new_entries.append(entry)
            await _write_sidecar(rec, family, data_dir)

    if new_entries:
        manifest.append_many(new_entries)
        logger.info(
            "metadata_complete",
            total=len(entries),
            new=len(new_entries),
            skipped=len(entries) - len(new_entries),
        )

    return entries
