"""Structured-data inventory workflows backed by SDK ingestion APIs."""

from __future__ import annotations

import csv
import json
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from urllib.parse import quote, urlparse

from google.auth.transport.requests import AuthorizedSession

from worai.core.google_oauth import SHEETS_SCOPES, load_google_credentials
from worai.core.ingest import DEFAULT_INGEST_LOADER, resolve_ingest_settings
from worai.core.url_sources.file import load_urls_from_file
from worai.core.url_sources.models import UrlSourceOptions
from worai.core.url_sources.resolver import resolve_url_records
from worai.core.url_sources.sheets import is_spreadsheet_source
from worai.errors import UsageError

DEFAULT_BASE_URL = "https://api.wordlift.io"
DEFAULT_TIMEOUT = 30.0


@dataclass
class InventoryRow:
    url: str
    faq_markup: str
    faq_markup_from_graph: str
    types: str
    structured_data: str


@dataclass
class InventoryOptions:
    source: str
    api_key: str
    base_url: str = DEFAULT_BASE_URL
    timeout: float = DEFAULT_TIMEOUT
    source_sheet_name: str | None = None
    url_regex: str | None = None
    output: str | None = None
    destination_spreadsheet_id: str | None = None
    destination_sheet_name: str | None = None
    client_secrets: str | None = None
    token: str = "oauth_token.json"
    port: int = 8080
    concurrency: str = "auto"
    ingest_source: str | None = None
    ingest_loader: str | None = None
    ingest_passthrough_when_html: bool | None = None
    ingest_timeout_ms: int | None = None
    ingest_retry_attempts: int | None = None
    ingest_retry_backoff_ms: int | None = None
    playwright_wait_until: str | None = None
    source_type: str | None = None


def _headers() -> list[str]:
    return ["url", "faq_markup", "faq_markup_from_graph", "types", "structured_data"]


def _normalize_loader(value: str) -> str:
    if value == "auto":
        return DEFAULT_INGEST_LOADER
    return value


def _infer_source_mode(source: str, sheet_name: str | None) -> str:
    if is_spreadsheet_source(source):
        return "sheets"

    path = Path(source)
    if path.exists():
        suffix = path.suffix.lower()
        if suffix in {".ttl", ".json"}:
            return "local"
        if suffix in {".xml", ".gz"}:
            return "sitemap"
        return "urls"

    parsed = urlparse(source)
    if parsed.scheme in {"http", "https", "file"}:
        return "sitemap"

    if sheet_name:
        return "sheets"

    raise UsageError(
        "Cannot infer ingest source from SOURCE. Pass --ingest-source explicitly "
        "(urls|sitemap|sheets|local)."
    )


def _build_source_bundle(options: InventoryOptions) -> dict[str, object]:
    resolved = resolve_ingest_settings(
        context="structured_data_inventory",
        new_source=options.ingest_source,
        new_loader=options.ingest_loader,
        new_passthrough_when_html=options.ingest_passthrough_when_html,
        legacy_source=options.source_type,
    )

    source_name = (
        _infer_source_mode(options.source, options.source_sheet_name)
        if resolved.source == "auto"
        else resolved.source
    )

    bundle: dict[str, object] = {
        "INGEST_SOURCE": source_name,
        "INGEST_LOADER": _normalize_loader(resolved.loader),
        "INGEST_PASSTHROUGH_WHEN_HTML": resolved.passthrough_when_html,
    }

    if options.url_regex:
        bundle["URL_REGEX"] = options.url_regex
    if options.ingest_timeout_ms is not None:
        bundle["INGEST_TIMEOUT_MS"] = options.ingest_timeout_ms
    if options.ingest_retry_attempts is not None:
        bundle["INGEST_RETRY_ATTEMPTS"] = options.ingest_retry_attempts
    if options.ingest_retry_backoff_ms is not None:
        bundle["INGEST_RETRY_BACKOFF_MS"] = options.ingest_retry_backoff_ms
    if options.playwright_wait_until:
        bundle["PLAYWRIGHT_WAIT_UNTIL"] = options.playwright_wait_until

    if source_name == "urls":
        source_path = Path(options.source)
        if not source_path.exists():
            raise UsageError("INGEST_SOURCE=urls requires a local URL list file source.")
        bundle["URLS"] = load_urls_from_file(source_path)
        return bundle

    if source_name == "sitemap":
        bundle["SITEMAP_URL"] = options.source
        return bundle

    if source_name == "sheets":
        if not options.source_sheet_name:
            raise UsageError("When source is a Google Spreadsheet, --sheet-name is required.")

        # Keep existing OAuth-based sheets UX in worai, then pass resolved URLs to SDK.
        records = resolve_url_records(
            UrlSourceOptions(
                source=options.source,
                timeout=options.timeout,
                sheet_name=options.source_sheet_name,
                url_regex=options.url_regex,
                client_secrets=options.client_secrets,
                token=options.token,
                port=options.port,
                ingest_source="sheets",
            )
        )
        bundle["INGEST_SOURCE"] = "urls"
        bundle.pop("URL_REGEX", None)
        bundle["URLS"] = [record.url for record in records]
        return bundle

    if source_name == "local":
        source_path = Path(options.source)
        if source_path.is_file() and source_path.suffix.lower() == ".json":
            bundle["INGEST_LOCAL_FILE"] = options.source
            return bundle

        # Preserve debug-cloud/local compatibility by passing resolved local items.
        records = resolve_url_records(
            UrlSourceOptions(
                source=options.source,
                timeout=options.timeout,
                sheet_name=options.source_sheet_name,
                url_regex=options.url_regex,
                client_secrets=options.client_secrets,
                token=options.token,
                port=options.port,
                ingest_source="local",
                source_type=options.source_type,
            )
        )
        items: list[dict[str, Any]] = []
        for index, record in enumerate(records):
            item: dict[str, Any] = {
                "id": f"local:{index}",
                "url": record.url,
            }
            if record.html is not None:
                item["html"] = record.html
            if record.metadata:
                item["metadata"] = record.metadata
            items.append(item)
        bundle["INGEST_LOCAL_ITEMS"] = items
        return bundle

    raise UsageError(f"Unsupported ingest source: {source_name}")


def _normalize_yes_no(value: Any) -> str:
    if isinstance(value, bool):
        return "yes" if value else "no"
    token = str(value or "").strip().lower()
    if token in {"yes", "true", "1"}:
        return "yes"
    if token in {"no", "false", "0", ""}:
        return "no"
    return token


def _normalize_structured_data(value: Any) -> str:
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    return json.dumps(value, ensure_ascii=False, separators=(",", ":"))


def _normalize_types(value: Any) -> str:
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    if isinstance(value, list):
        return ",".join(str(item) for item in value if str(item).strip())
    return str(value)


def _record_to_row(record: dict[str, Any]) -> InventoryRow:
    return InventoryRow(
        url=str(record.get("url", "")),
        faq_markup=_normalize_yes_no(record.get("faq_markup")),
        faq_markup_from_graph=_normalize_yes_no(record.get("faq_markup_from_graph")),
        types=_normalize_types(record.get("types")),
        structured_data=_normalize_structured_data(record.get("structured_data")),
    )


def _rows_from_csv(path: str) -> list[InventoryRow]:
    with open(path, "r", encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle)
        return [_record_to_row(row) for row in reader]


def build_inventory(
    options: InventoryOptions,
    *,
    on_progress: Callable[[dict[str, Any]], None] | None = None,
) -> list[InventoryRow]:
    try:
        from wordlift_sdk.ingestion import create_structured_data_inventory_from_ingestion
    except Exception as exc:  # pragma: no cover - depends on installed SDK
        raise UsageError(
            "create_structured_data_inventory_from_ingestion requires wordlift-sdk>=6.9.0."
        ) from exc

    source_bundle = _build_source_bundle(options)

    if options.output:
        output_csv = options.output
        cleanup_path: str | None = None
    else:
        tmp_file = tempfile.NamedTemporaryFile(prefix="worai-inventory-", suffix=".csv", delete=False)
        output_csv = tmp_file.name
        tmp_file.close()
        cleanup_path = output_csv

    try:
        result = create_structured_data_inventory_from_ingestion(
            source_bundle=source_bundle,
            api_key=options.api_key,
            base_url=options.base_url,
            output_csv=output_csv,
            on_progress=on_progress,
        )

        if hasattr(result, "to_dict"):
            records = result.to_dict(orient="records")
            if isinstance(records, list):
                return [_record_to_row(record) for record in records if isinstance(record, dict)]

        return _rows_from_csv(output_csv)
    except UsageError:
        raise
    except Exception as exc:
        detail = str(exc).strip()
        detail_suffix = (
            f" Details: {exc.__class__.__name__}: {detail}"
            if detail
            else f" Details: {exc.__class__.__name__}."
        )
        raise UsageError(
            "Failed to create structured-data inventory from ingestion source. "
            "Check source access, ingest settings, and SDK compatibility."
            f"{detail_suffix}"
        ) from exc
    finally:
        if cleanup_path:
            try:
                Path(cleanup_path).unlink(missing_ok=True)
            except Exception:
                pass


def write_csv(path: str, rows: list[InventoryRow]) -> None:
    with open(path, "w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=_headers())
        writer.writeheader()
        for row in rows:
            writer.writerow(row.__dict__)


def _get_sheet_titles(session: AuthorizedSession, spreadsheet_id: str) -> list[str]:
    url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}?fields=sheets.properties.title"
    response = session.get(url)
    if response.status_code != 200:
        raise RuntimeError(f"Failed to read spreadsheet metadata ({response.status_code}): {response.text}")
    data = response.json()
    sheets = data.get("sheets") or []
    titles: list[str] = []
    for sheet in sheets:
        title = ((sheet or {}).get("properties") or {}).get("title")
        if isinstance(title, str) and title:
            titles.append(title)
    return titles


def _ensure_sheet_exists(
    session: AuthorizedSession,
    spreadsheet_id: str,
    sheet_title: str,
) -> None:
    if sheet_title in _get_sheet_titles(session, spreadsheet_id):
        return
    url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}:batchUpdate"
    payload = {
        "requests": [
            {
                "addSheet": {
                    "properties": {"title": sheet_title},
                }
            }
        ]
    }
    response = session.post(url, json=payload)
    if response.status_code != 200:
        raise RuntimeError(
            f"Failed to create destination sheet '{sheet_title}' ({response.status_code}): {response.text}"
        )


def write_google_sheet(
    spreadsheet_id: str,
    sheet_name: str,
    rows: list[InventoryRow],
    *,
    client_secrets: str | None,
    token: str,
    port: int,
) -> None:
    creds = load_google_credentials(
        client_secrets_path=client_secrets,
        token_path=token,
        port=port,
        scopes=SHEETS_SCOPES,
    )
    session = AuthorizedSession(creds)

    _ensure_sheet_exists(session, spreadsheet_id, sheet_name)
    range_name = f"'{sheet_name}'!A1"
    encoded_range = quote(range_name, safe="")

    clear_url = (
        f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}/values/{encoded_range}:clear"
    )
    clear_resp = session.post(clear_url, json={})
    if clear_resp.status_code not in {200, 204}:
        raise RuntimeError(f"Failed to clear spreadsheet ({clear_resp.status_code}): {clear_resp.text}")

    values: list[list[str]] = [_headers()]
    for row in rows:
        values.append([
            row.url,
            row.faq_markup,
            row.faq_markup_from_graph,
            row.types,
            row.structured_data,
        ])

    update_url = (
        f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}/values/{encoded_range}"
        "?valueInputOption=RAW"
    )
    payload = {
        "range": range_name,
        "majorDimension": "ROWS",
        "values": values,
    }
    update_resp = session.put(update_url, json=payload)
    if update_resp.status_code != 200:
        raise RuntimeError(f"Failed to write spreadsheet ({update_resp.status_code}): {update_resp.text}")


def run_inventory(
    options: InventoryOptions,
    *,
    on_progress: Callable[[dict[str, Any]], None] | None = None,
) -> list[InventoryRow]:
    if bool(options.output) == bool(options.destination_spreadsheet_id):
        raise UsageError(
            "Provide exactly one destination: --output or --destination-sheet-id."
        )
    if options.destination_spreadsheet_id and not options.destination_sheet_name:
        raise UsageError(
            "--destination-sheet-name is required with --destination-sheet-id."
        )

    rows = build_inventory(options, on_progress=on_progress)

    if options.output:
        write_csv(options.output, rows)
    if options.destination_spreadsheet_id:
        write_google_sheet(
            options.destination_spreadsheet_id,
            options.destination_sheet_name or "",
            rows,
            client_secrets=options.client_secrets,
            token=options.token,
            port=options.port,
        )

    return rows
