from __future__ import annotations

import argparse
import logging
import multiprocessing
import os
import time
from typing import Any

from sqlalchemy import select

from db.session import get_session
from db.v2_jobs import claim_next_queue_item, mark_queue_item_done, mark_queue_item_failed


logger = logging.getLogger(__name__)

SUPPORTED_DOMAINS = ("product_data", "media", "relations")


def execution_mode() -> str:
    return str(os.getenv("V2_SYNC_EXECUTION_MODE", "simulate")).strip().lower() or "simulate"


def _simulate_processor(domain: str, row: Any) -> dict[str, Any]:
    payload = row.payload or {}
    return {
        "status": "simulated",
        "mode": execution_mode(),
        "domain": domain,
        "channel": row.channel,
        "sku": payload.get("sku"),
        "product_id": row.product_id,
        "note": "V2 queue lifecycle completed in simulation mode; channel adapter not configured.",
    }


def _resolve_connection(session, channel_code: str) -> dict[str, Any] | None:
    from db.compat_connections import compat_connection_id
    from db.models import ChannelConnection, MagentoConnection, ShopifyConnection

    channel = str(channel_code or "").strip()
    if not channel:
        return None

    magento = session.scalar(
        select(MagentoConnection).where(
            MagentoConnection.status == "active",
            MagentoConnection.store_code == channel,
        )
    )
    if magento is not None:
        return {
            "channel_type": "magento",
            "channel_code": channel,
            "native_connection_id": int(magento.id),
            "compat_connection_id": int(magento.id),
        }

    shopify = session.scalar(
        select(ShopifyConnection).where(
            ShopifyConnection.status == "active",
            ShopifyConnection.shop_code == channel,
        )
    )
    if shopify is not None:
        return {
            "channel_type": "shopify",
            "channel_code": channel,
            "native_connection_id": int(shopify.id),
            "compat_connection_id": compat_connection_id("shopify", int(shopify.id)),
            "shop_code": shopify.shop_code,
        }

    generic = session.scalar(
        select(ChannelConnection).where(
            ChannelConnection.status == "active",
            ChannelConnection.channel_code == channel,
        )
    )
    if generic is None:
        return None

    generic_type = str(generic.channel_type or "").strip().lower()
    if generic_type == "magento":
        return {
            "channel_type": "magento",
            "channel_code": channel,
            "native_connection_id": int(generic.id),
            "compat_connection_id": compat_connection_id("generic", int(generic.id)),
        }
    if generic_type == "shopify":
        return {
            "channel_type": "shopify",
            "channel_code": channel,
            "native_connection_id": int(generic.id),
            "compat_connection_id": compat_connection_id("generic", int(generic.id)),
            "shop_code": generic.store_code or channel,
        }
    return {
        "channel_type": generic_type or "generic",
        "channel_code": channel,
        "native_connection_id": int(generic.id),
        "compat_connection_id": compat_connection_id("generic", int(generic.id)),
    }


def _sync_magento_domain(session, *, domain: str, row: Any, connection: dict[str, Any]) -> dict[str, Any]:
    from app.jobs.magento_sync_worker import run_one as run_magento_sync_one
    from db.magento_repositories import SqlAlchemyMagentoSyncQueueRepository

    payload = row.payload or {}
    sku = str(payload.get("sku") or "").strip()
    if not sku:
        raise RuntimeError("V2 queue payload is missing sku for Magento sync")

    options: dict[str, Any] = {
        "dry_run": False,
        "use_master_catalog": True,
        "limit_skus": [sku],
        "run_pull_first": False,
        "run_pull_after": domain != "media",
    }
    if domain == "product_data":
        options.update(
            {
                "products_only": True,
                "force_upsert": True,
                "force_upsert_skus": [sku],
                "expand_relations": False,
            }
        )
    elif domain == "media":
        options.update(
            {
                "images_only": True,
                "force_images": True,
            }
        )
    elif domain == "relations":
        options.update(
            {
                "products_only": True,
                "force_relations": True,
                "expand_relations": True,
            }
        )
    else:
        raise RuntimeError(f"Unsupported Magento V2 domain '{domain}'")

    label = f"v2-{domain}-{sku}-{int(row.id)}"
    queue_repo = SqlAlchemyMagentoSyncQueueRepository(session)
    queue_id = int(queue_repo.enqueue(int(connection["native_connection_id"]), label, options=options))
    session.commit()
    processed = run_magento_sync_one(queue_id=queue_id)
    with get_session() as status_session:
        status = SqlAlchemyMagentoSyncQueueRepository(status_session).get_status(queue_id)
    if not processed or not status:
        raise RuntimeError(f"Magento sync queue item {queue_id} did not run")
    if status.get("status") != "done":
        raise RuntimeError(status.get("last_error") or f"Magento sync queue item {queue_id} failed")
    return {
        "status": "synced",
        "backend": "magento_sync_queue",
        "domain": domain,
        "sku": sku,
        "queue_id": queue_id,
        "queue_status": status.get("status"),
        "progress": status.get("progress") or {},
        "options": options,
    }


def _sync_shopify_domain(session, *, domain: str, row: Any, connection: dict[str, Any]) -> dict[str, Any]:
    payload = row.payload or {}
    sku = str(payload.get("sku") or "").strip()
    if not sku:
        raise RuntimeError("V2 queue payload is missing sku for Shopify sync")

    if domain == "media":
        from shopify.media_sync import push_product_images

        result = push_product_images(
            session,
            dry_run=False,
            skus=[sku],
            shop_code=connection.get("shop_code") or connection["channel_code"],
            connection_id=connection["compat_connection_id"],
            force=True,
        )
    elif domain in {"product_data", "relations"}:
        from shopify.product_sync import push_products

        result = push_products(
            session,
            dry_run=False,
            skus=[sku],
            shop_code=connection.get("shop_code") or connection["channel_code"],
            connection_id=connection["compat_connection_id"],
            force=True,
        )
    else:
        raise RuntimeError(f"Unsupported Shopify V2 domain '{domain}'")

    if str(result.get("status") or "").strip().lower() in {"disabled", "failed"} or result.get("failed"):
        raise RuntimeError(result.get("detail") or result.get("error") or f"Shopify {domain} sync failed")
    return {
        "status": "synced",
        "backend": "shopify",
        "domain": domain,
        "sku": sku,
        "result": result,
    }


def process_claimed_item(*, domain: str, row: Any) -> dict[str, Any]:
    mode = execution_mode()
    if mode == "simulate":
        processor = _simulate_processor
        return processor(domain, row)
    if mode != "live":
        raise RuntimeError(
            f"Unsupported V2 sync execution mode '{mode}'. Set V2_SYNC_EXECUTION_MODE to simulate or live."
        )
    with get_session() as session:
        connection = _resolve_connection(session, str(row.channel or ""))
        if connection is None:
            raise RuntimeError(f"Active channel connection '{row.channel}' not found")
        if connection["channel_type"] == "magento":
            return _sync_magento_domain(session, domain=domain, row=row, connection=connection)
        if connection["channel_type"] == "shopify":
            return _sync_shopify_domain(session, domain=domain, row=row, connection=connection)
        raise RuntimeError(f"Unsupported V2 channel type '{connection['channel_type']}' for channel '{row.channel}'")


def run_one(*, worker_id: str | None = None, domain: str | None = None) -> bool:
    worker = worker_id or f"v2-sync-worker-{os.getpid()}"
    with get_session() as session:
        claimed = claim_next_queue_item(session, worker_id=worker, domain=domain)
        session.commit()
        if claimed is None:
            return False
        claimed_domain, row = claimed
        row_id = int(row.id)
        logger.info(
            "v2-sync-worker: claimed domain=%s row=%s channel=%s product_id=%s attempt=%s",
            claimed_domain,
            row_id,
            row.channel,
            row.product_id,
            row.attempt_count,
        )
        try:
            response_payload = process_claimed_item(domain=claimed_domain, row=row)
            mark_queue_item_done(
                session,
                domain=claimed_domain,
                row_id=row_id,
                response_payload=response_payload,
            )
            session.commit()
            logger.info(
                "v2-sync-worker: completed domain=%s row=%s channel=%s product_id=%s",
                claimed_domain,
                row_id,
                row.channel,
                row.product_id,
            )
            return True
        except Exception as exc:
            session.rollback()
            mark_queue_item_failed(
                session,
                domain=claimed_domain,
                row_id=row_id,
                error=str(exc),
                response_payload={
                    "status": "failed",
                    "mode": execution_mode(),
                    "domain": claimed_domain,
                    "error": str(exc),
                },
            )
            session.commit()
            logger.exception(
                "v2-sync-worker: failed domain=%s row=%s channel=%s product_id=%s",
                claimed_domain,
                row_id,
                row.channel,
                row.product_id,
            )
            return True


def run_continuous_loop(*, worker_id: str, poll: int, domain: str | None = None) -> None:
    logger.info(
        "v2-sync-worker: starting continuous loop worker=%s domain=%s mode=%s poll=%ss",
        worker_id,
        domain or "all",
        execution_mode(),
        poll,
    )
    while True:
        try:
            had_work = run_one(worker_id=worker_id, domain=domain)
        except Exception:
            logger.exception("v2-sync-worker: pass failed worker=%s domain=%s", worker_id, domain or "all")
            time.sleep(poll)
            continue
        if not had_work:
            time.sleep(poll)


def _worker_process_entry(worker_index: int, poll: int, domain: str | None = None) -> None:
    worker_id = f"v2-sync-worker-{os.getpid()}-{worker_index}"
    run_continuous_loop(worker_id=worker_id, poll=poll, domain=domain)


def main() -> int:
    logging.basicConfig(level=logging.INFO)
    parser = argparse.ArgumentParser(description="Process V2 sync queues")
    parser.add_argument("--once", action="store_true", help="Process one V2 queue item and exit")
    parser.add_argument("--poll", type=int, default=15, help="Seconds to sleep when queues are empty")
    parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="Number of parallel V2 worker processes",
    )
    parser.add_argument(
        "--domain",
        choices=SUPPORTED_DOMAINS,
        default=None,
        help="Restrict processing to a single V2 queue domain",
    )
    args = parser.parse_args()
    worker = f"v2-sync-worker-{os.getpid()}"

    if args.once:
        run_one(worker_id=worker, domain=args.domain)
        return 0

    worker_count = max(1, int(args.workers))
    if worker_count == 1:
        run_continuous_loop(worker_id=worker, poll=args.poll, domain=args.domain)
        return 0

    processes: list[multiprocessing.Process] = []
    for index in range(worker_count):
        process = multiprocessing.Process(
            target=_worker_process_entry,
            args=(index, args.poll, args.domain),
            name=f"v2-sync-worker-{index}",
        )
        process.start()
        processes.append(process)
        logger.info("v2-sync-worker: spawned worker process=%s pid=%s", index, process.pid)

    for process in processes:
        process.join()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
