"""Background master taxonomy jobs (bootstrap, channel push, SKU reassignment, fix_all)."""

from __future__ import annotations

import argparse
import json
import logging
import os
import sys
from typing import Any, Dict, Optional

from db.master_taxonomy_jobs import (
    claim_next_master_taxonomy_job,
    enqueue_master_taxonomy_job,
    get_master_taxonomy_job,
    job_to_dict,
    mark_master_taxonomy_job_completed,
    mark_master_taxonomy_job_failed,
    update_master_taxonomy_job_progress,
)
from db.session import get_session

logger = logging.getLogger(__name__)


def get_master_taxonomy_job_status(job_id: str) -> Optional[Dict[str, Any]]:
    with get_session() as session:
        row = get_master_taxonomy_job(session, job_id)
        if row is None:
            return None
        return job_to_dict(row)


def start_master_taxonomy_job(job_type: str, params: Dict[str, Any]) -> str:
    """Enqueue a durable DB-backed job for channel_job_worker / master_taxonomy_job_worker."""
    with get_session() as session:
        row = enqueue_master_taxonomy_job(session, job_type, params)
        session.commit()
        return row.id


def _dedupe_ints(values: Optional[list[int]]) -> list[int]:
    out: list[int] = []
    seen: set[int] = set()
    for value in values or []:
        item = int(value)
        if item in seen:
            continue
        seen.add(item)
        out.append(item)
    return out


def _enforce_manual_taxonomy_fix_all_mode(taxonomy_mode: Optional[str]) -> tuple[str, str]:
    requested = str(taxonomy_mode or "link_only").strip().lower().replace("-", "_")
    if requested not in {"full", "link_only", "skip"}:
        raise ValueError("taxonomy_mode must be 'full', 'link_only', or 'skip'")
    if requested == "full":
        return "link_only", requested
    return requested, requested


def enqueue_fix_all_jobs(
    *,
    dry_run: bool,
    magento_connection_ids: Optional[list[int]] = None,
    shopify_connection_id: Optional[int] = None,
    pull_remotes: bool = True,
    create_missing: bool = True,
    push_products: bool = True,
    skus: Optional[list[str]] = None,
    reprocess_master: bool = False,
    taxonomy_mode: str = "link_only",
) -> Dict[str, Any]:
    """Enqueue one or more fix_all jobs without running the pipeline in the foreground."""
    mode, requested_mode = _enforce_manual_taxonomy_fix_all_mode(taxonomy_mode)
    magento_ids = _dedupe_ints(magento_connection_ids)
    if not magento_ids:
        magento_ids = [None]  # type: ignore[list-item]

    jobs = []
    for index, magento_id in enumerate(magento_ids):
        params = {
            "dry_run": dry_run,
            "magento_connection_id": magento_id,
            "shopify_connection_id": shopify_connection_id if index == 0 else None,
            "pull_remotes": pull_remotes,
            "create_missing": create_missing,
            "push_products": push_products,
            "skus": skus,
            "reprocess_master": bool(reprocess_master and index == 0),
            "taxonomy_mode": mode,
        }
        if requested_mode != mode:
            params["taxonomy_mode_requested"] = requested_mode
        job_id = start_master_taxonomy_job("fix_all", params)
        jobs.append({"job_id": job_id, "job_type": "fix_all", "params": params})
    return {"status": "queued", "job_count": len(jobs), "jobs": jobs}


def run_master_taxonomy_job_once(*, worker_id: str | None = None) -> bool:
    worker = worker_id or f"taxonomy-worker-{os.getpid()}"
    with get_session() as session:
        job = claim_next_master_taxonomy_job(session, worker_id=worker)
        session.commit()
        if job is None:
            return False
        job_id = job.id
        job_type = job.job_type
        params = dict(job.params or {})

    logger.info("taxonomy-worker: claimed job=%s type=%s", job_id, job_type)
    try:
        result = _execute(job_type, {**params, "_job_id": job_id})
        with get_session() as session:
            mark_master_taxonomy_job_completed(session, job_id, result=result)
            session.commit()
        logger.info("taxonomy-worker: job=%s type=%s status=completed", job_id, job_type)
    except Exception as exc:
        logger.exception("taxonomy-worker: job=%s type=%s failed", job_id, job_type)
        with get_session() as session:
            mark_master_taxonomy_job_failed(session, job_id, error=str(exc))
            session.commit()
    return True


def main() -> int:
    logging.basicConfig(level=logging.INFO)
    parser = argparse.ArgumentParser(description="Enqueue durable master taxonomy jobs")
    sub = parser.add_subparsers(dest="job_type", required=True)

    fix_all = sub.add_parser("fix_all", help="Enqueue the end-to-end master taxonomy pipeline")
    fix_all.add_argument("--apply", action="store_true", help="Persist changes (default is dry-run)")
    fix_all.add_argument("--sku", action="append", dest="skus", help="Limit SKU assignment/push; repeatable")
    fix_all.add_argument(
        "--magento-connection-id",
        type=int,
        action="append",
        default=None,
        help="Magento connection id; repeat to enqueue multiple Magento jobs",
    )
    fix_all.add_argument("--shopify-connection-id", type=int, default=None)
    fix_all.add_argument("--skip-pull-remotes", action="store_true")
    fix_all.add_argument("--skip-create-remotes", action="store_true")
    fix_all.add_argument("--skip-product-push", action="store_true")
    fix_all.add_argument("--reprocess-master", action="store_true")
    fix_all.add_argument(
        "--taxonomy-mode",
        choices=["full", "link-only", "link_only", "skip"],
        default="link_only",
        help="Manual-safe default is link-only; full is coerced to link-only for fix_all jobs.",
    )

    args = parser.parse_args()
    if args.job_type == "fix_all":
        result = enqueue_fix_all_jobs(
            dry_run=not args.apply,
            magento_connection_ids=args.magento_connection_id,
            shopify_connection_id=args.shopify_connection_id,
            pull_remotes=not args.skip_pull_remotes,
            create_missing=not args.skip_create_remotes,
            push_products=not args.skip_product_push,
            skus=args.skus,
            reprocess_master=args.reprocess_master,
            taxonomy_mode=args.taxonomy_mode,
        )
        print(json.dumps(result, indent=2, default=str))
        return 0

    raise ValueError(f"Unknown job type: {args.job_type}")


def _execute(job_type: str, params: Dict[str, Any]) -> Dict[str, Any]:
    if job_type == "collection_save":
        return _execute_collection_save(params)
    if job_type == "bootstrap":
        return _execute_bootstrap(params)
    if job_type == "push_nodes":
        return _execute_push_nodes(params)
    if job_type == "sku_assign":
        return _execute_sku_assign(params)
    if job_type == "apply_proposals":
        return _execute_apply_proposals(params)
    if job_type == "fix_all":
        return _execute_fix_all(params)
    raise ValueError(f"Unknown master taxonomy job type: {job_type}")


def _execute_collection_save(params: Dict[str, Any]) -> Dict[str, Any]:
    from db.collection_landing_pages import upsert_collection_landing_page

    payload = dict(params.get("payload") or {})
    if not payload:
        raise ValueError("collection_save payload is required")

    with get_session() as session:
        row = upsert_collection_landing_page(session, payload)
        session.commit()
    return {"status": "ok", "collection": row}


def _resolve_connections(params: Dict[str, Any]) -> tuple[Optional[int], Optional[int], Any, Any, Optional[str]]:
    from main import _default_native_connection_id, _resolve_native_connection_id

    from db.models import MagentoConnection, ShopifyConnection

    magento_api = None
    shopify_client = None
    shopify_shop_code = None
    native_magento_id = None
    native_shopify_id = None

    with get_session() as session:
        if params.get("magento_connection_id") is not None:
            native_magento_id = _resolve_native_connection_id(session, params["magento_connection_id"])
        elif params.get("include_magento", True):
            native_magento_id = _default_native_connection_id(session, "magento")

        if params.get("shopify_connection_id") is not None:
            native_shopify_id = _resolve_native_connection_id(session, params["shopify_connection_id"])
        elif params.get("include_shopify", True):
            native_shopify_id = _default_native_connection_id(session, "shopify")

        dry_run = bool(params.get("dry_run", False))
        create_missing = bool(params.get("create_missing", True))

        if not dry_run and native_magento_id:
            magento_connection = session.get(MagentoConnection, native_magento_id)
            if magento_connection and magento_connection.status == "active":
                from magento.magento_api import MagentoRestClient
                from magento.oauth_client import MagentoOAuthClient, build_magento_oauth_kwargs

                oauth = MagentoOAuthClient(**build_magento_oauth_kwargs(magento_connection))
                magento_api = MagentoRestClient(oauth)

        if not dry_run and native_shopify_id:
            shopify_connection = session.get(ShopifyConnection, native_shopify_id)
            if shopify_connection and shopify_connection.status == "active":
                from shopify.connections import build_client

                shopify_client = build_client(shopify_connection)
                shopify_shop_code = shopify_connection.shop_code

        return native_magento_id, native_shopify_id, magento_api, shopify_client, shopify_shop_code


def _execute_bootstrap(params: Dict[str, Any]) -> Dict[str, Any]:
    from db.master_taxonomy_sync import bootstrap_master_taxonomy

    dry_run = bool(params.get("dry_run", False))
    native_magento_id, native_shopify_id, magento_api, shopify_client, shopify_shop_code = _resolve_connections(
        params
    )
    with get_session() as session:
        result = bootstrap_master_taxonomy(
            session,
            dry_run=dry_run,
            magento_connection_id=native_magento_id,
            shopify_connection_id=native_shopify_id,
            run_backfill=bool(params.get("run_backfill", True)),
            run_build_hierarchy=bool(params.get("run_build_hierarchy", True)),
            run_sync_links=bool(params.get("run_sync_links", True)),
            push_to_channels=bool(params.get("push_to_channels", False)),
            magento_api=magento_api,
            shopify_client=shopify_client,
            shopify_shop_code=shopify_shop_code,
            create_missing_remotes=bool(params.get("create_missing", True)),
            allow_invent=bool(params.get("allow_taxonomy_invent", True)),
        )
        if not dry_run:
            session.commit()
    return result


def _execute_push_nodes(params: Dict[str, Any]) -> Dict[str, Any]:
    from db.master_taxonomy_sync import push_taxonomy_nodes_to_channels

    dry_run = bool(params.get("dry_run", False))
    native_magento_id, native_shopify_id, magento_api, shopify_client, shopify_shop_code = _resolve_connections(
        params
    )
    with get_session() as session:
        result = push_taxonomy_nodes_to_channels(
            session,
            node_ids=params.get("node_ids"),
            magento_connection_id=native_magento_id,
            shopify_connection_id=native_shopify_id,
            magento_api=magento_api,
            shopify_client=shopify_client,
            shopify_shop_code=shopify_shop_code,
            dry_run=dry_run,
            create_missing=bool(params.get("create_missing", True)),
            include_magento=bool(params.get("include_magento", True)),
            include_shopify=bool(params.get("include_shopify", True)),
            shopify_handle_overrides={
                int(node_id): str(handle)
                for node_id, handle in (params.get("shopify_handle_overrides") or {}).items()
                if str(handle or "").strip()
            },
            force_create_magento=bool(params.get("force_create_magento", False)),
            include_descendants=bool(params.get("include_descendants", True)),
        )
        if not dry_run:
            session.commit()
    return result


def _execute_fix_all(params: Dict[str, Any]) -> Dict[str, Any]:
    from app.jobs.sync_master_taxonomy_pipeline import run_master_taxonomy_pipeline

    dry_run = bool(params.get("dry_run", False))
    job_id = params.get("_job_id")
    taxonomy_mode, requested_mode = _enforce_manual_taxonomy_fix_all_mode(params.get("taxonomy_mode"))
    native_magento_id, native_shopify_id, _, _, _ = _resolve_connections(params)

    def progress_callback(stage: str, detail: Optional[Dict[str, Any]] = None) -> None:
        if not job_id:
            return
        payload: Dict[str, Any] = {"stage": stage, **(detail or {})}
        with get_session() as session:
            update_master_taxonomy_job_progress(
                session,
                job_id,
                progress=payload,
                notes=str(payload.get("notes") or stage.replace("_", " ")),
            )
            session.commit()

    result = run_master_taxonomy_pipeline(
        dry_run=dry_run,
        magento_connection_id=native_magento_id,
        shopify_connection_id=native_shopify_id,
        pull_remotes=bool(params.get("pull_remotes", True)),
        create_missing_remotes=bool(params.get("create_missing", True)),
        push_products=bool(params.get("push_products", True)),
        skus=params.get("skus"),
        reprocess_master=bool(params.get("reprocess_master", False)),
        progress_callback=progress_callback,
        taxonomy_mode=taxonomy_mode,
    )
    if requested_mode != taxonomy_mode:
        result["taxonomy_mode_requested"] = requested_mode
        result["taxonomy_mode_enforced"] = taxonomy_mode
        result["note"] = (
            "Manual taxonomy policy enforced: fix_all links to existing taxonomy only; "
            "automatic taxonomy/category/collection creation is disabled."
        )
    return result


def _execute_apply_proposals(params: Dict[str, Any]) -> Dict[str, Any]:
    from db.master_taxonomy_hierarchy import build_hierarchy_from_master_catalog
    from db.master_taxonomy_intent import apply_taxonomy_proposals

    dry_run = bool(params.get("dry_run", False))
    native_magento_id, native_shopify_id, magento_api, shopify_client, shopify_shop_code = _resolve_connections(
        params
    )
    with get_session() as session:
        result = apply_taxonomy_proposals(
            session,
            params.get("proposals") or [],
            dry_run=dry_run,
            sync_channels=bool(params.get("sync_channels", False)),
            magento_connection_id=native_magento_id,
            shopify_connection_id=native_shopify_id,
            magento_api=magento_api,
            shopify_client=shopify_client,
            shopify_shop_code=shopify_shop_code,
            allow_invent=bool(params.get("allow_taxonomy_invent", False)),
        )
        if not dry_run and bool(params.get("allow_taxonomy_invent", False)):
            build_hierarchy_from_master_catalog(session, dry_run=False, allow_invent=True)
            session.commit()
        elif not dry_run:
            session.commit()
    return result


def _execute_sku_assign(params: Dict[str, Any]) -> Dict[str, Any]:
    """Local DB assignment, then queue channel product pushes for affected SKUs."""
    import uuid

    from db.master_taxonomy_sync import assign_skus_to_taxonomy_node

    dry_run = bool(params.get("dry_run", False))
    node_id = int(params["node_id"])
    enqueue_channel_push = bool(params.get("enqueue_channel_push", True))
    sku_threshold = int(params.get("inline_sku_threshold", 25))

    with get_session() as session:
        assign_result = assign_skus_to_taxonomy_node(
            session,
            node_id=node_id,
            skus=params.get("skus"),
            only_assigned=bool(params.get("only_assigned", False)),
            category_l1=params.get("category_l1"),
            category_l2=params.get("category_l2"),
            category_l3=params.get("category_l3"),
            collection=params.get("collection"),
            product_family=params.get("product_family"),
            search=params.get("search"),
            replace_existing=bool(params.get("replace_existing", False)),
            dry_run=dry_run,
            limit=params.get("limit"),
            magento_connection_id=params.get("magento_connection_id"),
            shopify_connection_id=params.get("shopify_connection_id"),
        )
        if not dry_run:
            session.commit()

    sku_count = int(assign_result.get("sku_count") or 0)
    skus = list(assign_result.get("skus") or [])
    channel_jobs: Dict[str, Any] = {}

    # Always enqueue channel pushes — never inline Shopify push_products on the
    # request path. Cloudflare proxies time out (~100s) and return 502 HTML when
    # assign-skus?background=false waits on a live channel push.
    if not dry_run and enqueue_channel_push and sku_count > 0:
        channel_jobs = _enqueue_channel_pushes(skus, params)
        channel_jobs["inline_sku_threshold"] = sku_threshold

    return {"assignment": assign_result, "channel_jobs": channel_jobs}


def _push_skus_inline(skus: list[str], params: Dict[str, Any]) -> Dict[str, Any]:
    """Small SKU sets: push immediately via existing channel pipelines."""
    import uuid

    from main import _default_native_connection_id, _resolve_native_connection_id
    from shopify.product_sync import push_products

    out: Dict[str, Any] = {}
    with get_session() as session:
        if params.get("include_magento", True):
            magento_id = (
                _resolve_native_connection_id(session, params["magento_connection_id"])
                if params.get("magento_connection_id")
                else _default_native_connection_id(session, "magento")
            )
            if magento_id:
                from db.magento_repositories import SqlAlchemyMagentoSyncQueueRepository

                label = f"taxonomy-sku-assign-magento-{uuid.uuid4().hex[:8]}"
                queue_id = SqlAlchemyMagentoSyncQueueRepository(session).enqueue(
                    magento_id,
                    label,
                    options={
                        "dry_run": False,
                        "use_master_catalog": True,
                        "limit_skus": skus,
                        "expand_relations": False,
                    },
                    supersede_queued=False,
                )
                out["magento"] = {"status": "queued", "queue_id": queue_id, "label": label}

        if params.get("include_shopify", True):
            shopify_id = (
                _resolve_native_connection_id(session, params["shopify_connection_id"])
                if params.get("shopify_connection_id")
                else _default_native_connection_id(session, "shopify")
            )
            if shopify_id:
                summary = push_products(
                    session,
                    dry_run=False,
                    skus=skus,
                    connection_id=shopify_id,
                    force=True,
                )
                out["shopify"] = summary
        session.commit()
    return out


def _enqueue_channel_pushes(skus: list[str], params: Dict[str, Any]) -> Dict[str, Any]:
    from app.jobs.channel_jobs import enqueue_channel_job
    from main import _compat_id, _default_native_connection_id, _resolve_native_connection_id

    out: Dict[str, Any] = {}
    with get_session() as session:
        if params.get("include_magento", True):
            magento_id = (
                _resolve_native_connection_id(session, params["magento_connection_id"])
                if params.get("magento_connection_id")
                else _default_native_connection_id(session, "magento")
            )
            if magento_id:
                job = enqueue_channel_job(
                    session,
                    channel_connection_id=_compat_id("magento", magento_id),
                    channel_type="magento",
                    native_connection_id=magento_id,
                    channel_code="magento",
                    job_type="push",
                    dry_run=False,
                    mode="incremental",
                    notes="taxonomy SKU reassignment",
                    options={"limit_skus": skus, "use_master_catalog": True},
                )
                out["magento"] = {"status": "queued", "job_id": job.id}

        if params.get("include_shopify", True):
            shopify_id = (
                _resolve_native_connection_id(session, params["shopify_connection_id"])
                if params.get("shopify_connection_id")
                else _default_native_connection_id(session, "shopify")
            )
            if shopify_id:
                job = enqueue_channel_job(
                    session,
                    channel_connection_id=_compat_id("shopify", shopify_id),
                    channel_type="shopify",
                    native_connection_id=shopify_id,
                    channel_code="shopify",
                    job_type="push",
                    dry_run=False,
                    mode="incremental",
                    notes="taxonomy SKU reassignment",
                    options={"skus": skus, "force": True},
                )
                out["shopify"] = {"status": "queued", "job_id": job.id}
        session.commit()
    return out


if __name__ == "__main__":
    sys.exit(main())
