import logging
from enum import Enum, auto
from typing import TYPE_CHECKING, Union

from packaging import version

from mstrio import config
from mstrio.api import monitors
from mstrio.connection import Connection
from mstrio.helpers import (
    IServerError,
    MstrException,
    PartialSuccess,
    Success,
    VersionException,
)
from mstrio.server import Node, Project
from mstrio.utils.entity import Entity, EntityBase
from mstrio.utils.enum_helper import AutoName
from mstrio.utils.helper import exception_handler, validate_param_value
from mstrio.utils.monitors import all_nodes_async
from mstrio.utils.time_helper import DatetimeFormats, map_str_to_datetime
from mstrio.utils.version_helper import class_version_handler, method_version_handler

if TYPE_CHECKING:
    from mstrio.users_and_groups import User

logger = logging.getLogger(__name__)

ISERVER_VERSION_11_3_2 = '11.3.0200'


class DeliveryType(AutoName):
    RESERVED = auto()
    EMAIL = auto()
    FILE = auto()
    PRINTER = auto()
    CUSTOM = auto()
    INBOX = auto()
    CLIENT = auto()
    CACHE = auto()
    MOBILE = auto()
    BLACKBERRY = auto()
    IPHONE = auto()
    IPAD = auto()
    CAMPAIGN = auto()
    SNAPSHOT = auto()
    FTP = auto()
    ANDROID = auto()
    LAST_ONE = auto()
    COUNT = auto()
    ALL_INCLUDING_SNAPSHOT = auto()


class SortByV1(Enum):
    JOB_TYPE = 'jobType'
    USER_FULL_NAME = 'userFullName'
    OBJECT_ID = 'objectId'
    STATUS = 'status'
    PROJECT_ID = 'projectId'
    # check if valid as there are multiple
    # inconsistent information about sorting
    PROJECT_NAME = 'projectName'
    CREATION_TIME = 'creationTime'
    DESCRIPTION = 'description'
    JOB_ID = 'jobId'


class JobStatus(AutoName):
    READY = auto()
    EXECUTING = auto()
    WAITING = auto()
    COMPLETED = auto()
    ERROR = auto()
    CANCELING = auto()
    STOPPED = auto()
    WAITING_ON_GOVERNOR = auto()
    WAITING_FOR_AUTOPROMPT = auto()
    WAITING_FOR_PROJECT = auto()
    WAITING_FOR_CACHE = auto()
    WAITING_FOR_CHILDREN = auto()
    WAITING_FOR_RESULTS = auto()
    LOADING_PROMPT = auto()
    RESOLVING_DESTINATION = auto()
    DELIVERING = auto()
    EXPORTING = auto()
    CACHE_READY = auto()
    WAITING_FOR_DI_FILE = auto()
    WAITING_FOR_CONFLICT_RESOLVE = auto()
    STEP_PAUSING = auto()


class JobType(AutoName):
    INTERACTIVE = auto()
    SUBSCRIPTION = auto()
    PREDICTIVE_CACHE = auto()


# NOTE: new endpoint Enums below:
class ObjectType(AutoName):
    OTHERS = auto()
    REPORT = auto()
    CUBE = auto()
    DOCUMENT = auto()
    DASHBOARD = auto()

    @classmethod
    def _missing_(cls, value):
        """Handle missing values in ObjectType enum."""
        if value == 'dossier':
            return cls.DASHBOARD
        else:
            return super()._missing_(value)


class PUName(AutoName):
    BROWSING = auto()
    RESOLUTION = auto()
    QUERY_EXECUTIONS = 'query_execution'
    ANALYTICAL = auto()
    SQL_ENGINE = auto()
    DATA_FORMATTING = auto()
    NCS = auto()
    REST_ASYNCHRONOUS = auto()


class SubscriptionType(AutoName):
    RESERVED = auto()
    EMAIL = auto()
    PRINTER = auto()
    CUSTOM = auto()
    INBOX = auto()
    CLIENT = auto()
    CACHE = auto()
    MOBILE = auto()
    MOBILE_BLACKBERRY = auto()
    MOBILE_IPHONE = auto()
    MOBILE_IPAD = auto()
    CAMPAIGN = auto()
    SNAPSHOT = auto()
    FTP = auto()
    MOBILE_ANDROID = auto()
    LAST_ONE = auto()
    COUNT = auto()
    ALL = auto()
    ALL_INCLUDING_SNAPSHOT = auto()


class SortBy(Enum):
    ID_ASC = '+id'
    ID_DESC = '-id'
    TYPE_ASC = '+type'
    TYPE_DESC = '-type'
    STATUS_ASC = '+status'
    STATUS_DESC = '-status'
    USER_ASC = '+user'
    USER_DESC = '-user'
    DESCRIPTION_ASC = '+description'
    DESCRIPTION_DESC = '-description'
    OBJECT_TYPE_ASC = '+objectType'
    OBJECT_TYPE_DESC = '-objectType'
    OBJECT_ID_ASC = '+objectId'
    OBJECT_ID_DESC = '-objectId'
    SUBSCRIPTION_TYPE_ASC = '+subscriptionType'
    SUBSCRIPTION_TYPE_DESC = '-subscriptionType'
    PROCESSING_UNIT_PRIORITY_ASC = '+processingUnitPriority'
    PROCESSING_UNIT_PRIORITY_DESC = '-processingUnitPriority'
    CREATION_TIME_ASC = '+creationTime'
    CREATION_TIME_DESC = '-creationTime'
    COMPLETED_TASKS_ASC = '+completedTasks'
    COMPLETED_TASKS_DESC = '-completedTasks'
    PROJECT_ID_ASC = '+projectId'
    PROJECT_ID_DESC = '-projectId'
    PROJECT_NAME_ASC = '+projectName'
    PROJECT_NAME_DESC = '-projectName'
    SUBSCRIPTION_RECIPIENT_ASC = '+subscriptionRecipient'
    SUBSCRIPTION_RECIPIENT_DESC = '-subscriptionRecipient'
    MEMORY_USAGE_ASC = '+memoryUsage'
    MEMORY_USAGE_DESC = '-memoryUsage'
    ELAPSED_TIME_ASC = '+elapsedTime'
    ELAPSED_TIME_DESC = '-elapsedTime'


@method_version_handler('11.3.0200')
def _set_api_wrappers(connection) -> dict:
    if connection.iserver_version and version.parse(
        connection.iserver_version
    ) == version.parse(ISERVER_VERSION_11_3_2):
        return {
            (
                'id',
                'description',
                'status',
                'type',
                'priority',
                'processing_unit_priority',
                'warehouse_priority',
                'object_id',
                'user',
                'project_name',
                'creation_time',
                'total_tasks',
                'completed_tasks',
                'filter_name',
                'template_name',
                'sql',
                'subscription_owner',
                'subscription_recipient',
                'destination',
            ): monitors.get_job
        }
    else:
        return {
            (
                'id',
                'type',
                'status',
                'user',
                'description',
                'object_type',
                'object_id',
                'parent_id',
                'child_ids',
                'subscription_type',
                'processing_unit_priority',
                'step_id',
                'pu_name',
                'creation_time',
                'completed_tasks',
                'total_tasks',
                'project_name',
                'subscription_recipient',
                'memory_usage',
                'elapsed_time',
                'step_elapsed_time',
                'filter_name',
                'template_name',
                'sql',
                'subscription_owner',
                'error_time',
                'error_message',
                'step_statistics',
            ): monitors.get_job_v2
        }


@method_version_handler('11.3.0200')
def list_jobs(
    connection: "Connection",
    node: Node | str | None = None,
    user: Union["User", str] | None = None,
    description: str | None = None,
    type: JobType | str | None = None,
    status: JobStatus | str | None = None,
    object_id: str | None = None,
    object_type: ObjectType | None = None,
    project: Project | str | None = None,
    pu_name: PUName | str | None = None,
    subscription_type: SubscriptionType | str | None = None,
    subscription_recipient: Union["User", str] | None = None,
    memory_usage: str | None = None,
    elapsed_time: str | None = None,
    sort_by: SortBy | str | None = None,
    to_dictionary: bool = False,
    limit: int | None = None,
    **filters,
) -> list["Job"] | list[dict]:
    """List jobs objects or job dictionaires.
    Args:
        connection(object): Strategy One connection object returned by
            `connection.Connection()`
        node(Node, str, optional): Node object or name, if not passed list jobs
            on all nodes
        user(User, str, optional): Field to filter on job owner's full name
        (exact match)
        description(str, optional): Field to filter on job description (partial
            match)
        type(JobType, optional): Field to filter on job type (exact match),
        status(JobStatus, optional): Field to filter on job status (exact match)
        object_id(str, optional): Field to filter on object type (exact
            match)
        object_type(ObjectType, optional): Field to filter on object type (exact
            match)
        project(Project, str, optional): Field to filter on project name (exact
            match)
        pu_name(PUName, optional): Field to filter on processing unit name
            (exact match)
        subscription_type(SubscriptionType, optional): Field to filter on
        subscription type (exact match)
        subscription_recipient(User, str, optional): Field to filter on
            subscription recipient's full name (exact match)
        memory_usage(str, optional): Field to filter on the job elapsed time,
            for example 'gt:100' means filtering jobs with memory usage greater
            than to 100 MB. Valid operators are:
            gt - greater than
            lt - less than
        elapsed_time(str, optional): Field to filter on the job elapsed time,
            for example 'gt:100' means filtering jobs with elapsed time greater
            than 100 milliseconds(11.3.3+) or seconds(11.3.2). Valid operators:
            gt - greater than
            lt - less than
        sort_by(SortBy, optional): Specify sorting criteria, for example
            SortBy.STATUS_ASC means sorting status is ascending order
            or SortBy.USER_DESC means sorting user in descending order.
            Currently, the server supports sorting only by single field.
        to_dictionary(bool, optional): if True, return Schedules as
            list of dicts
        limit(int, optional): maximum number of schedules returned
        **filters(optional): Local filter parameters

    Examples:
        >>> list_jobs_v1(connection, duration='gt:100')

    Returns:
        List[Job] | List[dict]: list of Job objects or dictionaries.
    """
    user = user.full_name if isinstance(user, Entity) else user
    subscription_recipient = (
        subscription_recipient.full_name
        if isinstance(user, Entity)
        else subscription_recipient
    )
    if project:
        project = (
            project
            if isinstance(project, Project)
            else Project(connection, name=project)
        )

    if isinstance(object_type, str) and object_type.lower() == 'dashboard':
        object_type = ObjectType.DASHBOARD

    # depending on version call either one or the other
    if connection.iserver_version and version.parse(
        connection.iserver_version
    ) == version.parse(ISERVER_VERSION_11_3_2):
        filters = __prepare_v1_request(
            description,
            object_type,
            pu_name,
            subscription_type,
            subscription_recipient,
            memory_usage,
            elapsed_time,
            filters,
        )
        return list_jobs_v1(
            connection=connection,
            node=node,
            project=project,
            status=status,
            job_type=type,
            user=user,
            limit=limit,
            to_dictionary=to_dictionary,
            **filters,
        )
    else:
        project_name = project.name if project else None
        node_name = node.name if isinstance(node, Node) else node
        msg = "Error fetching chunk of jobs."
        objects = all_nodes_async(
            connection,
            async_api=monitors.get_jobs_v2_async,
            filters=filters,
            error_msg=msg,
            unpack_value='jobs',
            limit=limit,
            node_name=node_name,
            user=user,
            description=description,
            type=type,
            object_id=object_id,
            object_type=object_type,
            project_name=project_name,
            status=status,
            pu_name=pu_name,
            subscription_type=subscription_type,
            subscription_recipient=subscription_recipient,
            memory_usage=memory_usage,
            elapsed_time=elapsed_time,
            sort_by=sort_by,
            fields=['jobs'],
        )

    if to_dictionary:
        return objects
    else:
        return [Job.from_dict(source=obj, connection=connection) for obj in objects]


@method_version_handler('11.3.0200')
def list_jobs_v1(
    connection: Connection,
    node: Node | str | None = None,
    project: Project | str | None = None,
    status: JobStatus | str | None = None,
    job_type: JobType | str | None = None,
    user: Union["User", str] | None = None,
    object_id: str | None = None,
    sort_by: SortBy | str | None = None,
    to_dictionary: bool = False,
    limit: int | None = None,
    **filters,
) -> list["Job"] | list[dict]:
    """List job objects or job dictionaries. Optionally filter list.
    NOTE: list_jobs can return up to 1024 jobs per request.

    Args:
        connection(object): Strategy One connection object returned by
            'connection.Connection()'
        node(Node, str, optional): Node object or name, if not passed list jobs
            on all nodes
        project(str, Project, optional): Project id or object to filter by
        status(JobStatus, optional): Job status to filter by
        job_type(JobType, optional): Job type to filter by
        user(User, str, optional): User object or full name to filter by
        object_id(str, optional): Object id to filter by
        sort_by(SortBy, optional): Specifies sorting criteria to sort by
        to_dictionary(bool, optional): if True, return Schedules as
            list of dicts
        limit(int, optional): maximum number of schedules returned.
        **filters: Available filter parameters:[id, description, status,
            jobType, duration, jobId, objectd]
            - duration(str, optional): Field to filter on the job elapsed time,
                for example 'gt:100' means filtering jobs with elapsed time
                greater than to 100 seconds. Valid operators:
                gt - greater than
                lt - less than

    Examples:
        >>> list_jobs_v1(connection, duration='gt:100')

    Returns:
        List[Job] | List[dict]: list of Job objects or dictionaries.
    """
    project_id = project.id if isinstance(project, Project) else project
    user_full_name = user.full_name if isinstance(user, Entity) else user

    if 'duration' in filters:
        filters['duration'] = __elapsed_filtering(filters['duration'])

    node_name = node.name if isinstance(node, Node) else node
    msg = "Error fetching chunk of jobs."
    objects = all_nodes_async(
        connection,
        async_api=monitors.get_jobs_async,
        filters=filters,
        error_msg=msg,
        unpack_value='jobs',
        limit=limit,
        node_name=node_name,
        project_id=project_id,
        status=status,
        job_type=job_type,
        user_full_name=user_full_name,
        object_id=object_id,
        sort_by=sort_by,
    )

    if to_dictionary:
        return objects
    else:
        return [Job.from_dict(source=obj, connection=connection) for obj in objects]


@method_version_handler('11.3.0200')
def kill_jobs(
    connection: Connection, jobs: list[Union["Job", str]]
) -> Success | PartialSuccess | MstrException:
    """Kill existing jobs by Job objects or job ids.

    Args:
        connection(object): Strategy One connection object returned by
            'connection.Connection()'
        jobs: List of Job objects or job ids to kill

    Returns:
        Success: object if all jobs were killed
        PartialSuccess: if not all jobs were killed
        MstrException: otherwise
    """
    validate_param_value("jobs", jobs, list)
    jobs = [job.id if isinstance(job, Job) else job for job in jobs]
    return monitors.cancel_jobs(connection, jobs)


@method_version_handler('11.3.0200')
def kill_all_jobs(
    connection: Connection,
    user: Union["User", str] | None = None,
    description: str | None = None,
    type: JobType | str | None = None,
    status: JobStatus | str | None = None,
    object_id: str | None = None,
    object_type: ObjectType | str | None = None,
    project: Project | str | None = None,
    pu_name: PUName | str | None = None,
    subscription_type: SubscriptionType | str | None = None,
    subscription_recipient: Union["User", str] | None = None,
    memory_usage: str | None = None,
    elapsed_time: str | None = None,
    force: bool = False,
    **filters,
) -> Success | PartialSuccess | MstrException:
    """Kill jobs filtered by passed fields

    Args:
        connection(object): Strategy One connection object returned by
            `connection.Connection()`
         node(Node, str, optional): Node object or name, if not passed kill jobs
            on all nodes
        user(User, str, optional): Field to filter on job owner's full name
        (exact match)
        description(str, optional): Field to filter on job description (partial
            match)
        type(JobType, optional): Field to filter on job type (exact match),
        status(JobStatus, optional): Field to filter on job status (exact match)
        object_id(str, optional): Field to filter on object id (exact
            match)
        object_type(ObjectType, optional): Field to filter on object type (exact
            match)
        project(Project, str, optional): Field to filter on project name (exact
            match)
        pu_name(PUName, optional): Field to filter on processing unit name
            (exact match)
        subscription_type(SubscriptionType, optional): Field to filter on
        subscription type (exact match)
        subscription_recipient(User, str, optional): Field to filter on
            subscription recipient's full name (exact match)
        memory_usage(str, optional): Field to filter on the job elapsed time,
            for example 'gt:100' means filtering jobs with memory usage greater
            than to 100 MB. Valid operators are:
            gt - greater than
            lt - less than
        elapsed_time(str, optional): Field to filter on the job elapsed time,
            for example 'gt:100' means filtering jobs with elapsed time greater
            than 100 milliseconds(11.3.3+) or seconds(11.3.2). Valid operators:
            gt - greater than
            lt - less than
        force(bool, optional): Force flag

    Returns:
        Success: object if all jobs were killed
        PartialSuccess: if not all jobs were killed
        MstrException: otherwise
    """

    jobs = list_jobs(
        connection,
        user,
        description,
        type,
        status,
        object_id,
        object_type,
        project,
        pu_name,
        subscription_type,
        subscription_recipient,
        memory_usage,
        elapsed_time,
        filters,
    )
    assert jobs, "No jobs to kill"

    jobs_ids = [job.id for job in jobs]
    if not force:
        print(f"Found jobs: {jobs}")
        user_input = input("Are you sure you want to kill those jobs?[Y/N]: ") or 'N'
    if force or user_input == 'Y':
        return kill_jobs(connection, jobs_ids)


def __prepare_v1_request(
    description: str,
    object_type: str,
    pu_name: str,
    subscription_type: str,
    subscription_recipient: str,
    memory_usage: str,
    elapsed_time: str,
    filters: dict,
) -> dict:
    # raise VersionException if parameter not supported in v1
    unsupported_parameters = [
        description,
        object_type,
        pu_name,
        subscription_type,
        subscription_recipient,
        memory_usage,
    ]
    params_str = (
        "description, object_type, pu_name, subscription_type, "
        "subscription_recipient, memory_usage"
    )
    if any(unsupported_parameters):
        msg = (
            f"Passed unsupported parameter for this version of IServer "
            f"({ISERVER_VERSION_11_3_2}) Parameters supported only in 11.3.3+ "
            f"versions: {params_str}"
        )
        raise VersionException(msg)

    if elapsed_time:
        filters['duration'] = elapsed_time
    return filters


def __elapsed_filtering(elapsed: str) -> str:
    if (
        isinstance(elapsed, str)
        and len(elapsed) > 3
        and (elapsed.startswith('gt:') or elapsed.startswith('lt:'))
    ):
        if elapsed.startswith('gt'):
            return f'>{elapsed[3:]}'
        elif elapsed.startswith('lt'):
            return f'<{elapsed[3:]}'
        else:
            return elapsed
    else:
        raise TypeError('Incorrect "duration" format, correct example: "gt:100" ')


@class_version_handler('11.3.0200')
class Job(EntityBase):
    """Python representation of a Job object.
    Note: Some functionality is missing when working with 11.3.2 I-Server
        versions. To use all functionality 11.3.3 and up will be required.
        There are also some differences between 11.3.2 and 11.3.3+ Job
        attributes values.

    Attributes:
        connection: A Strategy One connection object
        id: Job information id
        description: Description of the job
        status: Status of the job
        type: Type of the job
        object_id: Id of object
        object_name: Name of object
        object_type: Type of object
        parent_id: Parent ID
        childs_ids: Array of children IDs
        subscription_recipient: Subscription recipient of the job
        subscription_type: Subscription delivery type of the job
        processing_unit_priority: Job Processing Unit Priority
        step_id: Step ID of the job
        user: Full name of the job initiator
        user_id: ID of the job initiator
        username: Username of the job initiator
        project_name: Name of the project in which the job is active
        project_id: Id of the project in which the job is active
        pu_name: Processing Unit name
        creation_time: Creation time of the job
        elapsed_time: Time elapsed since the creation of the job in milliseconds
            (11.3.3+) or seconds (11.3.2)
        step_elapsed_time: Time elapsed since the creation of the step
            in milliseconds
        memory_usage: Memory usage of the job in bytes
        client_machine: Client machine running the job
        total_tasks: Total tasks of the job
        completed_tasks: Completed tasks of the job
        error_message: Error message for the job
        error_time: Error time of the job
        filter_name: Name of the filter for the job
        sql: SQL of the job
        subscription_owner: Subscription owner
        template_name: Name of the template for the job
        step_statistics: Step statistics for the job
    """

    _FROM_DICT_MAP = {
        'creation_time': DatetimeFormats.FULLDATETIME,
        'error_time': DatetimeFormats.FULLDATETIME,
        'type': JobType,
        'status': JobStatus,
        'object_type': ObjectType,
        'subscription_type': SubscriptionType,
        'pu_name': PUName,
    }
    _API_GETTERS = {
        (
            'id',
            'type',
            'status',
            'user',
            'description',
            'object_type',
            'object_id',
            'object_name',
            'parent_id',
            'child_ids',
            'subscription_type',
            'processing_unit_priority',
            'step_id',
            'pu_name',
            'creation_time',
            'completed_tasks',
            'total_tasks',
            'project_name',
            'subscription_recipient',
            'memory_usage',
            'elapsed_time',
            'step_elapsed_time',
            'filter_name',
            'template_name',
            'sql',
            'subscription_owner',
            'error_time',
            'error_message',
            'step_statistics',
        ): monitors.get_job_v2
    }
    _REST_ATTR_MAP = {
        "job_id": None,  # delete job_id and only use id
        "job_type": "type",
        "user_full_name": "user",
        "total_task": "total_tasks",
        "completed_task": "completed_tasks",
        "filter": "filter_name",
        "template": "template_name",
        "machine": "client_machine",
        "priority": None,
        "warehouse_priority": None,
        "destination": None,
        "project_id": None,
        "username": None,
        "delivery_type": None,
        "duration": "elapsed_time",
    }

    def __init__(self, connection: Connection, id: str) -> None:
        """Initialize the Job object, populates it with I-Server data.

        Args:
            connection: Strategy One connection object returned
                by `connection.Connection()`
            id: Job information id
        """
        Job._API_GETTERS = _set_api_wrappers(connection)
        super().__init__(connection, object_id=id)

    def _init_variables(self, **kwargs):
        """Initialize variables given kwargs.

        Note: attributes not accepted by any implementation of this function
            in the inheritance chain will be disregarded.
        """
        kwargs = self._rest_to_python(kwargs)
        # create _AVAILABLE_ATTRIBUTES map
        self._AVAILABLE_ATTRIBUTES.update(
            {key: type(val) for key, val in kwargs.items()}
        )

        self._connection = kwargs.get('connection')
        self._id = kwargs.get('id')
        self._description = kwargs.get('description')
        self._status = JobStatus(kwargs.get('status')) if kwargs.get('status') else None
        self._processing_unit_priority = kwargs.get('processing_unit_priority')
        self._creation_time = map_str_to_datetime(
            "creation_time", kwargs.get("creation_time"), self._FROM_DICT_MAP
        )
        self._elapsed_time = kwargs.get('elapsed_time')
        self._project_name = kwargs.get('project_name')
        self._object_id = kwargs.get('object_id')
        self._object_type = (
            ObjectType(kwargs.get('object_type')) if kwargs.get('object_type') else None
        )
        self._object_name = kwargs.get('object_name')
        self._sql = kwargs.get('sql')
        self._subscription_owner = kwargs.get('subscription_owner')
        self._subscription_recipient = kwargs.get('subscription_recipient')
        self._client_machine = kwargs.get('client_machine')
        self._type = JobType(kwargs.get('type')) if kwargs.get('type') else None
        self._user = kwargs.get('user')
        self._total_tasks = kwargs.get('total_tasks')
        self._completed_tasks = kwargs.get('completed_tasks')
        self._parent_id = kwargs.get('parent_id')
        self._child_ids = kwargs.get('child_ids')
        self._subscription_type = (
            SubscriptionType(kwargs.get('subscription_type'))
            if kwargs.get('subscription_type')
            else kwargs.get('subscription_type')
        )
        self._step_id = kwargs.get('step_id')
        self._pu_name = PUName(kwargs.get('pu_name')) if kwargs.get('pu_name') else None
        self._memory_usage = kwargs.get('memory_usage')
        self._step_elapsed_time = kwargs.get('step_elapsed_time')
        self._filter_name = kwargs.get('filter_name')
        self._template_name = kwargs.get('template_name')
        self._error_time = (
            map_str_to_datetime(
                "error_time", kwargs.get("error_time"), self._FROM_DICT_MAP
            )
            if kwargs.get("error_time")
            else None
        )
        self._error_message = kwargs.get('error_message')
        self._step_statistics = kwargs.get('step_statistics')
        self._user_id = None
        self._username = None

    def kill(self) -> bool:
        """Kill the job.

        Returns:
            True if successfully killed job, False otherwise.
        """
        response = monitors.cancel_job(self._connection, str(self.id))
        if config.verbose:
            success_msg = f"Successfully killed {self.id}."
            fail_msg = f"Error killing job {self.id}."
            msg = success_msg if response.ok else fail_msg
            logger.info(msg)
        return response.ok

    def list_properties(self, excluded_properties: list[str] | None = None) -> dict:
        """List Job object properties."""
        properties = super().list_properties(excluded_properties)
        properties.pop('connection', None)
        return properties

    def refresh_status(self) -> None:
        """Refresh the job status."""
        try:
            self.fetch()
        except IServerError as e:
            if e.http_code == 500 or e.http_code == 404:
                stable_states = [
                    JobStatus.COMPLETED,
                    JobStatus.ERROR,
                    JobStatus.STOPPED,
                ]
                if self._status and self._status not in stable_states:
                    self._status = JobStatus.COMPLETED

    @property
    def status(self):
        return self._status

    @property
    def description(self):
        return self._description

    @property
    def processing_unit_priority(self):
        return self._processing_unit_priority

    @property
    def creation_time(self):
        return self._creation_time

    @property
    def project_name(self):
        return self._project_name

    @property
    def object_type(self):
        return self._object_type

    @property
    def object_id(self):
        return self._object_id

    @property
    def sql(self):
        return self._sql

    @property
    def subscription_owner(self):
        return self._subscription_owner

    @property
    def subscription_recipient(self):
        return self._subscription_recipient

    @property
    def client_machine(self):
        return self._client_machine

    @property
    def user(self):
        return self._user

    @property
    def total_tasks(self):
        return self._total_tasks

    @property
    def completed_tasks(self):
        return self._completed_tasks

    @property
    def parent_id(self):
        return self._parent_id

    @property
    def child_ids(self):
        return self._child_ids

    @property
    def subscription_type(self):
        return self._subscription_type

    @property
    def step_id(self):
        return self._step_id

    @property
    def pu_name(self):
        return self._pu_name

    @property
    def memory_usage(self):
        return self._memory_usage

    @property
    def elapsed_time(self):
        return self._elapsed_time

    @property
    def step_elapsed_time(self):
        return self._step_elapsed_time

    @property
    def filter_name(self):
        return self._filter_name

    @property
    def template_name(self):
        return self._template_name

    @property
    def error_time(self):
        return self._error_time

    @property
    def error_message(self):
        return self._error_message

    @property
    def step_statistics(self):
        return self._step_statistics

    @property
    def object_name(self):
        return self._object_name

    @property
    def user_id(self):
        from mstrio.users_and_groups import User

        if self._user:
            if not self._user_id:
                users = User._get_user_ids(
                    connection=self._connection,
                    name_begins=self._user,
                    name=self._user,
                )
                if users:
                    [self._user_id] = users
                else:
                    exception_handler(
                        f"There is no user: '{self._user}'", exception_type=ValueError
                    )
            return self._user_id
        else:
            return None

    @property
    def username(self):
        from mstrio.users_and_groups import User

        if self._user:
            if not self._username:
                if self._user_id:
                    self._username = User(self._connection, id=self._user_id).username
                else:
                    user = User(self._connection, name=self._user)
                    self._username = user.username
                    self._user_id = user.id
            return self._username
        else:
            return None
