from __future__ import annotations

import argparse
import logging
import multiprocessing
import os
import time

from app.jobs.channel_jobs import claim_next_channel_job, run_channel_job
from app.jobs.master_taxonomy_enqueue import run_master_taxonomy_job_once
from db.session import get_session


logger = logging.getLogger(__name__)


def run_one(*, worker_id: str | None = None) -> bool:
    worker = worker_id or f"channel-worker-{os.getpid()}"
    with get_session() as session:
        job = claim_next_channel_job(session, worker_id=worker)
        # Persist the claim (and any stale-job resets) before executing, so a
        # rollback during execution cannot silently un-claim the job.
        session.commit()
        if job is None:
            return False
        logger.info("channel-worker: claimed job=%s type=%s channel=%s", job.id, job.job_type, job.channel_code)
        result = run_channel_job(session, job)
        session.commit()
        _log_channel_job_result(job.id, job.job_type, result)
        return True


def _log_channel_job_result(job_id: int, job_type: str, result: dict) -> None:
    status = result.get("status")
    logger.info("channel-worker: job=%s type=%s status=%s", job_id, job_type, status)
    failed = result.get("failed")
    error_count = result.get("error_count")
    if failed or error_count:
        logger.warning(
            "channel-worker: job=%s failed=%s error_count=%s pushed=%s message=%s",
            job_id,
            failed,
            error_count,
            result.get("pushed") or result.get("success_count"),
            result.get("message") or result.get("detail") or result.get("error"),
        )
    errors = result.get("errors") or []
    for sample in errors[:5]:
        logger.warning(
            "channel-worker: job=%s sku=%s error=%s",
            job_id,
            sample.get("sku") or sample.get("channel_sku"),
            sample.get("error"),
        )
    if result.get("message") and not failed and not error_count:
        logger.info("channel-worker: job=%s message=%s", job_id, result.get("message"))


def run_scheduler_pass(*, worker_id: str | None = None) -> None:
    from app.jobs.channel_scheduler import enqueue_due_schedules

    result = enqueue_due_schedules(worker_id=worker_id)
    if result.get("enqueued"):
        logger.info("channel-worker: scheduler enqueued=%s", result["enqueued"])


def run_continuous_loop(
    *,
    worker_id: str,
    poll: int,
    no_scheduler: bool,
) -> None:
    logger.info(
        "channel-worker: starting continuous loop worker=%s scheduler=%s poll=%ss",
        worker_id,
        "off" if no_scheduler else "on",
        poll,
    )
    while True:
        try:
            had_work = run_one(worker_id=worker_id)
        except Exception:
            # DB hiccup or similar infrastructure failure — stay alive and retry.
            logger.exception("channel-worker: pass failed worker=%s", worker_id)
            time.sleep(poll)
            continue
        if not had_work:
            try:
                had_work = run_master_taxonomy_job_once(worker_id=worker_id)
            except Exception:
                logger.exception("channel-worker: taxonomy pass failed worker=%s", worker_id)
        if not had_work:
            if not no_scheduler:
                try:
                    run_scheduler_pass(worker_id=worker_id)
                except Exception:
                    logger.exception("channel-worker: scheduler pass failed worker=%s", worker_id)
            time.sleep(poll)


def _worker_process_entry(worker_index: int, poll: int, no_scheduler: bool) -> None:
    worker_id = f"channel-worker-{os.getpid()}-{worker_index}"
    run_continuous_loop(worker_id=worker_id, poll=poll, no_scheduler=no_scheduler)


def main() -> int:
    logging.basicConfig(level=logging.INFO)
    parser = argparse.ArgumentParser(description="Process channel_job queue")
    parser.add_argument("--once", action="store_true", help="Process one job and exit")
    parser.add_argument("--poll", type=int, default=30, help="Seconds to sleep when queue is empty")
    parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="Number of parallel worker processes (each claims jobs via DB row locking)",
    )
    parser.add_argument(
        "--no-scheduler",
        action="store_true",
        help="Do not enqueue due channel_schedule rows from this worker (run channel_scheduler separately)",
    )
    args = parser.parse_args()
    worker = f"channel-worker-{os.getpid()}"

    if args.once:
        if not run_one(worker_id=worker):
            run_master_taxonomy_job_once(worker_id=worker)
        return 0

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

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

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


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