"""Graph sync workflow integration."""

from __future__ import annotations

import asyncio
import json
import logging
import os
import re
import tempfile
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from worai.core.ingest import DEFAULT_INGEST_LOADER, resolve_ingest_settings

try:
    from wordlift_sdk.configuration import ConfigurationProvider
    from wordlift_sdk.kg_build import (
        ProfileImportProtocol,
        load_profile_config,
    )
    from wordlift_sdk.kg_build.container import KgBuildApplicationContainer
except ModuleNotFoundError:  # pragma: no cover - environment dependent
    ConfigurationProvider = None
    ProfileImportProtocol = None
    load_profile_config = None
    KgBuildApplicationContainer = None


LOGGER = logging.getLogger("worai.graph")
_TIMEOUT_RE = re.compile(r"^(?P<value>\d+(?:\.\d+)?)(?P<unit>ms|s)?$")


@dataclass
class GraphSyncOptions:
    profile: str
    config_path: Path = Path("worai.toml")
    debug: bool = False


def load_service_account_json(raw_value: str | None) -> str:
    if not raw_value:
        raise ValueError("Missing profile setting: sheets_service_account")

    candidate_path = Path(raw_value)
    if candidate_path.exists() and candidate_path.is_file():
        return candidate_path.read_text(encoding="utf-8")

    try:
        parsed = json.loads(raw_value)
    except json.JSONDecodeError as exc:
        raise ValueError("sheets_service_account must be JSON content or a file path") from exc

    if not isinstance(parsed, dict):
        raise ValueError("sheets_service_account JSON must be an object")
    return raw_value


def build_extra_settings(profile_settings: dict[str, Any]) -> dict[str, Any]:
    consumed = {
        "urls",
        "sitemap_url",
        "sitemap_url_pattern",
        "sheets_url",
        "sheets_name",
        "sheets_service_account",
        "overwrite",
        "concurrency",
        "web_page_import_mode",
        "web_page_import_timeout",
        "ingest_source",
        "ingest_loader",
        "ingest_passthrough_when_html",
        "google_search_console",
    }
    return {key.upper(): value for key, value in profile_settings.items() if key not in consumed}


def _append_py_setting(lines: list[str], key: str, value: Any) -> None:
    lines.append(f"{key} = {repr(value)}")


def _using_sheets_source(settings: dict[str, Any]) -> bool:
    return bool(settings.get("sheets_url") or settings.get("sheets_name"))


def _parse_bool(value: Any, *, name: str) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in {"1", "true", "yes", "on"}:
            return True
        if lowered in {"0", "false", "no", "off"}:
            return False
    raise ValueError(f"{name} must be a boolean")


def _resolve_web_page_import_timeout_ms(value: Any) -> int:
    if value is None:
        return 60000

    parsed: float
    explicit_ms = False
    explicit_s = False
    if isinstance(value, (int, float)):
        parsed = float(value)
    elif isinstance(value, str):
        token = value.strip().lower()
        if not token:
            raise ValueError("web_page_import_timeout must not be empty")
        match = _TIMEOUT_RE.match(token)
        if not match:
            raise ValueError(
                "web_page_import_timeout must be a number of seconds (e.g. 60 or 60s) "
                "or milliseconds (e.g. 60000ms)"
            )
        parsed = float(match.group("value"))
        explicit_ms = match.group("unit") == "ms"
        explicit_s = match.group("unit") == "s"
    else:
        raise ValueError(
            "web_page_import_timeout must be numeric seconds or a string with optional s/ms suffix"
        )

    if parsed <= 0:
        raise ValueError("web_page_import_timeout must be greater than 0")

    if explicit_ms:
        return int(parsed)
    if explicit_s:
        return int(parsed * 1000)

    return int(parsed * 1000)


def _resolve_sdk_fetch_mode(ingest_loader: str) -> str:
    if ingest_loader in {"proxy", "premium_scraper"}:
        return ingest_loader
    return "default"


def resolve_google_search_console(config_path: Path, profile_name: str) -> bool:
    try:
        data = tomllib.loads(config_path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return False
    except tomllib.TOMLDecodeError as exc:
        raise ValueError(f"Invalid TOML config: {config_path}: {exc}") from exc

    global_value = data.get("google_search_console", False)
    profile_table = data.get("profile") or data.get("profiles") or {}
    profile_value = profile_table.get(profile_name, {}).get("google_search_console")
    selected = profile_value if profile_value is not None else global_value
    return _parse_bool(selected, name="google_search_console")


def resolve_postprocessor_runtime(config_path: Path, profile_name: str) -> str | None:
    try:
        data = tomllib.loads(config_path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return None
    except tomllib.TOMLDecodeError as exc:
        raise ValueError(f"Invalid TOML config: {config_path}: {exc}") from exc

    profile_table = data.get("profile") or data.get("profiles") or {}
    profile_value = profile_table.get(profile_name, {}).get("postprocessor_runtime")
    global_value = data.get("postprocessor_runtime")
    selected = profile_value if profile_value is not None else global_value
    if selected is None:
        return None
    if not isinstance(selected, str):
        raise ValueError("postprocessor_runtime must be a string")

    runtime = selected.strip().lower()
    if runtime not in {"subprocess", "persistent"}:
        raise ValueError("postprocessor_runtime must be one of: subprocess, persistent")
    return runtime


def _build_dynamic_settings(
    profile_api_key: str,
    settings: dict[str, Any],
    *,
    google_search_console: bool,
) -> tuple[list[str], str | None]:
    lines = [f"WORDLIFT_KEY = {repr(profile_api_key)}"]
    sheets_service_account_json: str | None = None

    urls = settings.get("urls")
    sitemap_url = settings.get("sitemap_url")
    sitemap_url_pattern = settings.get("sitemap_url_pattern")
    sheets_url = settings.get("sheets_url")
    sheets_name = settings.get("sheets_name")
    using_sheets = _using_sheets_source(settings)
    ingest = resolve_ingest_settings(
        context="graph_sync",
        new_source=settings.get("ingest_source"),
        new_loader=settings.get("ingest_loader"),
        new_passthrough_when_html=settings.get("ingest_passthrough_when_html"),
        legacy_loader=settings.get("web_page_import_mode"),
        default_loader=DEFAULT_INGEST_LOADER,
    )

    if urls:
        _append_py_setting(lines, "URLS", list(urls))
    elif sitemap_url:
        _append_py_setting(lines, "SITEMAP_URL", sitemap_url)
        if sitemap_url_pattern:
            _append_py_setting(lines, "SITEMAP_URL_PATTERN", sitemap_url_pattern)
    elif using_sheets:
        if not sheets_url or not sheets_name:
            raise ValueError("Both sheets_url and sheets_name are required when using Google Sheets source")
        sheets_service_account_json = load_service_account_json(settings.get("sheets_service_account"))
        _append_py_setting(lines, "SHEETS_URL", sheets_url)
        _append_py_setting(lines, "SHEETS_NAME", sheets_name)
    else:
        raise ValueError(
            "One source is required: urls, sitemap_url, or sheets_url+sheets_name+sheets_service_account"
        )

    source_value = ingest.source
    if source_value == "auto":
        if urls:
            source_value = "urls"
        elif sitemap_url:
            source_value = "sitemap"
        elif using_sheets:
            source_value = "sheets"
    _append_py_setting(lines, "INGEST_SOURCE", source_value)
    _append_py_setting(lines, "INGEST_LOADER", ingest.loader)
    _append_py_setting(lines, "INGEST_PASSTHROUGH_WHEN_HTML", ingest.passthrough_when_html)
    _append_py_setting(lines, "WEB_PAGE_IMPORT_MODE", _resolve_sdk_fetch_mode(ingest.loader))
    _append_py_setting(
        lines,
        "WEB_PAGE_IMPORT_TIMEOUT",
        _resolve_web_page_import_timeout_ms(settings.get("web_page_import_timeout")),
    )
    _append_py_setting(lines, "CONCURRENCY", int(settings.get("concurrency", 4)))
    _append_py_setting(lines, "OVERWRITE", bool(settings.get("overwrite", False)))
    _append_py_setting(lines, "GOOGLE_SEARCH_CONSOLE", google_search_console)

    for key, value in build_extra_settings(settings).items():
        _append_py_setting(lines, key, value)

    return lines, sheets_service_account_json


async def _run_dynamic_cloud_workflow(
    *,
    lines: list[str],
    sheets_service_account_json: str | None,
    debug: bool,
    debug_profile_name: str,
    protocol_factory,
) -> None:
    temp_sa_path: str | None = None
    temp_config_path: str | None = None
    debug_dir = Path("output") / "debug_cloud" / debug_profile_name if debug else None

    try:
        if debug_dir:
            debug_dir.mkdir(parents=True, exist_ok=True)
            LOGGER.info("Debug mode enabled. Saving intermediate graphs to: %s", debug_dir)

        if sheets_service_account_json:
            with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f_sa:
                f_sa.write(sheets_service_account_json)
                temp_sa_path = f_sa.name
            _append_py_setting(lines, "SHEETS_SERVICE_ACCOUNT", temp_sa_path)

        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f_cfg:
            f_cfg.write("\n".join(lines) + "\n")
            temp_config_path = f_cfg.name

        provider = ConfigurationProvider.create(temp_config_path)
        container = KgBuildApplicationContainer(provider)
        context = await container.get_context()
        protocol = protocol_factory(context, debug_dir=debug_dir, workflow_config=None)
        if hasattr(protocol, "__await__"):
            protocol = await protocol
        container.set_protocol(protocol)
        workflow = await container.create_kg_import_workflow()
        await workflow.run()
    finally:
        if temp_config_path and os.path.exists(temp_config_path):
            os.remove(temp_config_path)
        if temp_sa_path and os.path.exists(temp_sa_path):
            os.remove(temp_sa_path)


async def run_graph_sync_async(options: GraphSyncOptions) -> None:
    if (
        ConfigurationProvider is None
        or ProfileImportProtocol is None
        or load_profile_config is None
        or KgBuildApplicationContainer is None
    ):
        raise ValueError("wordlift-sdk kg_build support is unavailable in this Python environment")

    config = load_profile_config(options.config_path)
    profile = config.get(options.profile)
    if profile is None:
        raise ValueError(f"Profile '{options.profile}' not found in {options.config_path}")
    if not profile.api_key:
        raise ValueError(f"Profile '{profile.name}' is missing api_key")

    settings = dict(profile.settings)
    google_search_console = resolve_google_search_console(options.config_path, options.profile)
    postprocessor_runtime = resolve_postprocessor_runtime(options.config_path, options.profile)
    lines, sheets_service_account_json = _build_dynamic_settings(
        profile.api_key,
        settings,
        google_search_console=google_search_console,
    )

    async def protocol_factory(context, debug_dir=None, workflow_config=None):
        return ProfileImportProtocol(
            context=context,
            profile=profile,
            root_dir=Path.cwd(),
            debug_dir=debug_dir,
        )

    previous_postprocessor_runtime = os.environ.get("POSTPROCESSOR_RUNTIME")
    try:
        if postprocessor_runtime is not None:
            os.environ["POSTPROCESSOR_RUNTIME"] = postprocessor_runtime

        await _run_dynamic_cloud_workflow(
            lines=lines,
            sheets_service_account_json=sheets_service_account_json,
            debug=options.debug,
            debug_profile_name=profile.name,
            protocol_factory=protocol_factory,
        )
    finally:
        if postprocessor_runtime is not None:
            if previous_postprocessor_runtime is None:
                os.environ.pop("POSTPROCESSOR_RUNTIME", None)
            else:
                os.environ["POSTPROCESSOR_RUNTIME"] = previous_postprocessor_runtime


def run_graph_sync(options: GraphSyncOptions) -> None:
    asyncio.run(run_graph_sync_async(options))
