# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""debusine-admin command to print server configuration information."""

import datetime as dt  # pragma: no cover
from typing import Any, NoReturn, override  # pragma: no cover

from debusine.db.models import WorkRequest, Worker  # pragma: no cover
from debusine.django.management.debusine_base_command import (
    DebusineBaseCommand,
)  # pragma: no cover
from debusine.server.scheduler import (
    get_provided_worker_tags,
    get_required_task_tags,
)  # pragma: no cover


def format_date(value: dt.datetime | None) -> str:  # pragma: no cover
    """Format a datetime that could possibly be None."""
    return "never" if value is None else value.strftime("%Y-%m-%d")


class Command(DebusineBaseCommand):  # pragma: no cover
    """Command to check support for scheduler tags."""

    help = (
        "Check to see possible issues before turning on tag-based scheduling."
    )

    def print_work_request(self, wr: WorkRequest) -> None:
        """Format a work request in one line."""
        pending = format_date(wr.pending_at)
        started = format_date(wr.started_at)
        self.stdout.write(
            f"#{wr.id} created:{wr.created_at:%Y-%m-%d}"
            f" pending:{pending} started:{started}"
            f" created_by:{wr.created_by.username} status:{wr.status}"
            f" type:{wr.task_type} name:{wr.task_name}"
        )

    def check_workers_without_tags(self) -> None:
        """Check workers without tags."""
        workers_without_tags = Worker.objects.exclude(
            static_metadata__has_key="provided_tags"
        ).exclude(dynamic_metadata__has_key="provided_tags")
        if workers_without_tags.exists():
            self.stdout.write(
                f"Found {workers_without_tags.count()} workers"
                " without worker-provided tags:"
            )
            for worker in workers_without_tags:
                connected = format_date(worker.connected_at)
                self.stdout.write(
                    f"{worker.name} registered:{worker.registered_at:%Y-%m-%d}"
                    f" connected:{connected}"
                )
        else:
            self.stdout.write("Found no workers without worker-provided tags.")

    def check_work_requests_without_tags(self) -> None:
        """Check work requests without tags."""
        work_requests_without_tags = WorkRequest.objects.filter(
            status__in=(
                WorkRequest.Statuses.PENDING,
                WorkRequest.Statuses.RUNNING,
            ),
            scheduler_tags_provided__len=0,
            scheduler_tags_required__len=0,
        ).select_related("created_by")
        if work_requests_without_tags.exists():
            self.stdout.write(
                f"Found {work_requests_without_tags.count()} pending/running"
                " work_requests without task-provided or worker-required tags:"
            )
            for wr in work_requests_without_tags:
                self.print_work_request(wr)
        else:
            self.stdout.write(
                "Found no work_requests without worker-provided tags."
            )

    def check_scheduled_work_requests(self) -> None:
        """Check that scheduled work requests can be scheduled with tags."""
        work_requests_assigned = (
            WorkRequest.objects.filter(status=WorkRequest.Statuses.RUNNING)
            .exclude(worker=None)
            .select_related("worker")
        )
        assigned: int = work_requests_assigned.count()
        unschedulable: list[WorkRequest] = []
        for wr in work_requests_assigned:
            assert wr.worker is not None
            provided_worker_tags = get_provided_worker_tags(wr.worker)
            required_task_tags = get_required_task_tags(wr.worker)
            if not (
                set(wr.scheduler_tags_required) <= provided_worker_tags
                and required_task_tags <= set(wr.scheduler_tags_provided)
            ):
                unschedulable.append(wr)
        if assigned and unschedulable:
            self.stdout.write(
                f"{len(unschedulable)} unschedulable work requests among the"
                f" {assigned} assigned to workers:"
            )
            for wr in unschedulable:
                self.print_work_request(wr)
        elif assigned:
            self.stdout.write(
                f"No unschedulable work requests among the {assigned}"
                " assigned to workers."
            )
        else:
            self.stdout.write("No work requests currently assigned to workers.")

    def check_unschedulable_work_requests(self) -> None:
        """Check that pending work requests can be scheduled by one worker."""
        pending_wrs = WorkRequest.objects.filter(
            status=WorkRequest.Statuses.PENDING
        )
        if pending_wrs.exists():
            schedulable_wrs: set[int] = set()
            for worker in Worker.objects.all():
                provided_worker_tags = get_provided_worker_tags(worker)
                required_task_tags = get_required_task_tags(worker)
                for wr in pending_wrs.filter(
                    scheduler_tags_required__contained_by=list(
                        provided_worker_tags
                    ),
                    scheduler_tags_provided__contains=list(required_task_tags),
                ):
                    schedulable_wrs.add(wr.id)
            unschedulable_wrs = pending_wrs.exclude(
                id__in=list(schedulable_wrs)
            )
            if unschedulable_wrs.exists():
                self.stdout.write(
                    f"{unschedulable_wrs.count()}/{pending_wrs.count()}"
                    " pending work requests cannot be scheduled based on tags:"
                )
                for wr in unschedulable_wrs:
                    self.print_work_request(wr)
            else:
                self.stdout.write(
                    "All pending work requests can be scheduled using tags"
                    " by at least one of the current workers."
                )
        else:
            self.stdout.write(
                "There are no pending work requests to check for workers"
                " available for them via tags."
            )

    @override
    def handle(self, *args: Any, **options: Any) -> NoReturn:
        self.check_workers_without_tags()
        self.stdout.write()
        self.check_work_requests_without_tags()
        self.stdout.write()
        self.check_scheduled_work_requests()
        self.stdout.write()
        self.check_unschedulable_work_requests()
        self.stdout.write()
        raise SystemExit(0)
