# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from __future__ import print_function
import webbrowser

from knack.log import get_logger
from knack.util import CLIError
from azext_devops.devops_sdk.exceptions import AzureDevOpsServiceError
from azext_devops.devops_sdk.v5_0.work_item_tracking.models import JsonPatchOperation, Wiql
from azext_devops.dev.common.arguments import convert_date_string_to_iso8601
from azext_devops.dev.common.identities import (ME, get_current_identity,
                                                resolve_identity,
                                                get_account_from_identity)
from azext_devops.dev.common.services import (get_work_item_tracking_client,
                                              resolve_instance,
                                              resolve_instance_and_project)
from azext_devops.dev.common.uri import uri_quote

logger = get_logger(__name__)


def create_work_item(work_item_type, title, description=None, assigned_to=None, area=None,
                     iteration=None, reason=None, discussion=None, fields=None, open=False,  # pylint: disable=redefined-builtin
                     organization=None, project=None, detect=None):
    r"""Create a work item.
    :param work_item_type: Name of the work item type (e.g. Bug).
    :type work_item_type: str
    :param title: Title of the work item.
    :type title: str
    :param description: Description of the work item.
    :type description: str
    :param assigned_to: Name of the person the work item is assigned-to (e.g. fabrikam).
    :type assigned_to: str
    :param area: Area the work item is assigned to (e.g. Demos)
    :type area: str
    :param iteration: Iteration path of the work item (e.g. Demos\Iteration 1).
    :type iteration: str
    :param reason: Reason for the state of the work item.
    :type reason: str
    :param discussion: Comment to add to a discussion in a work item.
    :type discussion: str
    :param fields: Space separated "field=value" pairs for custom fields you would like to set.
    In case of multiple fields : "field1=value1" "field2=value2".
    Refer https://aka.ms/azure-devops-cli-field-api for more details on fields.
    :type fields: [str]
    :param open: Open the work item in the default web browser.
    :type open: bool
    :rtype: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItem>`
    """
    try:
        organization, project = resolve_instance_and_project(
            detect=detect, organization=organization, project=project, project_required=True)
        patch_document = []
        if title is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.Title', title))
        else:
            raise ValueError('--title is a required argument.')
        if description is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.Description', description))
        if assigned_to is not None:
            # 'assigned to' does not take an identity id.  Display name works.
            assigned_to = assigned_to.strip()
            if assigned_to == '':
                resolved_assigned_to = ''
            else:
                resolved_assigned_to = _resolve_identity_as_unique_user_id(assigned_to, organization)
            if resolved_assigned_to is not None:
                patch_document.append(_create_work_item_field_patch_operation('add', 'System.AssignedTo',
                                                                              resolved_assigned_to))
        if area is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.AreaPath', area))
        if iteration is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.IterationPath', iteration))
        if reason is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.Reason', reason))
        if discussion is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.History', discussion))
        if fields is not None and fields:
            for field in fields:
                kvp = field.split('=', 1)
                if len(kvp) == 2:
                    patch_document.append(_create_work_item_field_patch_operation('add', kvp[0], kvp[1]))
                else:
                    raise ValueError('The --fields argument should consist of space separated "field=value" pairs.')
        client = get_work_item_tracking_client(organization)
        work_item = client.create_work_item(document=patch_document, project=project, type=work_item_type)
        if open:
            _open_work_item(work_item, organization)
        return work_item
    except AzureDevOpsServiceError as ex:
        _handle_vsts_service_error(ex)


def update_work_item(id, title=None, description=None, assigned_to=None, state=None, area=None,  # pylint: disable=redefined-builtin
                     iteration=None, reason=None, discussion=None, fields=None, open=False,  # pylint: disable=redefined-builtin
                     organization=None, detect=None):
    r"""Update work items.
    :param id: The id of the work item to update.
    :type id: int
    :param title: Title of the work item.
    :type title: str
    :param description: Description of the work item.
    :type description: str
    :param assigned_to: Name of the person the work item is assigned-to (e.g. fabrikam).
    :type assigned_to: str
    :param state: State of the work item (e.g. active).
    :type state: str
    :param area: Area the work item is assigned to (e.g. Demos).
    :type area: str
    :param iteration: Iteration path of the work item (e.g. Demos\Iteration 1).
    :type iteration: str
    :param reason: Reason for the state of the work item.
    :type reason: str
    :param discussion: Comment to add to a discussion in a work item.
    :type discussion: str
    :param fields: Space separated "field=value" pairs for custom fields you would like to set.
    Refer https://aka.ms/azure-devops-cli-field-api for more details on fields.
    :type fields: [str]
    :param open: Open the work item in the default web browser.
    :type open: bool
    :rtype: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItem>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    patch_document = []
    if title is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.Title', title))
    if description is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.Description', description))
    if assigned_to is not None:
        assigned_to = assigned_to.strip()
        # 'assigned to' does not take an identity id.  Display name works.
        if assigned_to == '':
            resolved_assigned_to = ''
        else:
            resolved_assigned_to = _resolve_identity_as_unique_user_id(assigned_to, organization)
        if resolved_assigned_to is not None:
            patch_document.append(_create_work_item_field_patch_operation('add', 'System.AssignedTo',
                                                                          resolved_assigned_to))
    if state is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.State', state))
    if area is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.AreaPath', area))
    if iteration is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.IterationPath', iteration))
    if reason is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.Reason', reason))
    if discussion is not None:
        patch_document.append(_create_work_item_field_patch_operation('add', 'System.History', discussion))
    if fields is not None and fields:
        for field in fields:
            kvp = field.split('=', 1)
            if len(kvp) == 2:
                patch_document.append(_create_work_item_field_patch_operation('add', kvp[0], kvp[1]))
            else:
                raise ValueError('The --fields argument should consist of space separated "field=value" pairs.')
    client = get_work_item_tracking_client(organization)
    work_item = client.update_work_item(document=patch_document, id=id)
    if open:
        _open_work_item(work_item, organization)
    return work_item


def delete_work_item(id,  # pylint: disable=redefined-builtin
                     destroy=False, organization=None, project=None, detect=None):
    """Delete a work item.
    :param id: Unique id of the work item.
    :type id: int
    :param destroy: Permanently delete this work item.
    :type destroy: bool
    :rtype: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItemDelete>`
    """
    try:
        organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project)
        client = get_work_item_tracking_client(organization)
        delete_response = client.delete_work_item(id=id, project=project, destroy=destroy)
        print('Deleted work item {}'.format(id))
        return delete_response
    except AzureDevOpsServiceError as ex:
        _handle_vsts_service_error(ex)


def _handle_vsts_service_error(ex):
    logger.debug(ex, exc_info=True)
    if ex.type_key == 'RuleValidationException' and "FieldReferenceName" in ex.custom_properties:
        if ex.message is not None:
            message = ex.message
            if message and message[len(message) - 1] != '.':
                message += '.'
            name = ex.custom_properties["FieldReferenceName"]
            if name is not None:
                if name in _SYSTEM_FIELD_ARGS:
                    message += ' Use the --{} argument to supply this value.'.format(_SYSTEM_FIELD_ARGS[name])
                else:
                    message += ' To specify a value for this field, use the --field argument and set the name of the ' \
                               + 'name/value pair to {}.'.format(name)
        else:
            message = "RuleValidationException for FieldReferenceName: " + ex.custom_properties["FieldReferenceName"]
        raise CLIError(ValueError(message))

    raise CLIError(ex)


def show_work_item(id, as_of=None, expand='all', fields=None, open=False, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Show details for a work item.
    :param id: The ID of the work item
    :type id: int
    :param as_of: Work item details as of a particular date and time. Provide a date or date time string.
    Assumes local time zone. Example: '2019-01-20', '2019-01-20 00:20:00'.
    For UTC, append 'UTC' to the date time string, '2019-01-20 00:20:00 UTC'.
    :type as_of:string
    :param expand: The expand parameters for work item attributes.
    :type expand:str
    :param fields: Comma-separated list of requested fields. Example:System.Id,System.AreaPath.
    Refer https://aka.ms/azure-devops-cli-field-api for more details on fields.
    :type fields: str
    :param open: Open the work item in the default web browser.
    :type open: bool
    :rtype: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItem>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    try:
        client = get_work_item_tracking_client(organization)
        as_of_iso = None
        if as_of:
            as_of_iso = convert_date_string_to_iso8601(value=as_of, argument='as-of')
        if fields:
            fields = fields.split(',')
        work_item = client.get_work_item(id, as_of=as_of_iso, fields=fields, expand=expand)
    except AzureDevOpsServiceError as ex:
        _handle_vsts_service_error(ex)

    if open:
        _open_work_item(work_item, organization)
    return work_item


# pylint: disable=too-many-statements
def query_work_items(wiql=None, id=None, path=None, organization=None, project=None, detect=None):  # pylint: disable=redefined-builtin
    """Query for a list of work items. Only supports flat queries.
    :param wiql: The query in Work Item Query Language format.  Ignored if --id or --path is specified.
    :type wiql: str
    :param id: The ID of an existing query.  Required unless --path or --wiql are specified.
    :type id: str
    :param path: The path of an existing query.  Ignored if --id is specified.
    :type path: str
    :rtype: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItem>`
    """
    if wiql is None and path is None and id is None:
        raise CLIError("Either the --wiql, --id, or --path argument must be specified.")
    organization, project = resolve_instance_and_project(
        detect=detect, organization=organization, project=project, project_required=False)
    client = get_work_item_tracking_client(organization)
    if id is None and path is not None:
        if project is None:
            raise CLIError("The --project argument must be specified for this query.")
        query = client.get_query(project=project, query=path)
        id = query.id
    if id is not None:
        query_result = client.query_by_id(id=id)
    else:
        wiql_object = Wiql()
        wiql_object.query = wiql
        query_result = client.query_by_wiql(wiql=wiql_object)
    if query_result.work_items:
        _last_query_result[_LAST_QUERY_RESULT_KEY] = query_result  # store query result for table view
        safety_buffer = 100  # a buffer in the max url length to protect going over the limit
        remaining_url_length = 2048 - safety_buffer
        remaining_url_length -= len(organization)
        # following subtracts relative url, the asof parameter and beginning of id and field parameters.
        # asof value length will vary, but this should be the longest possible
        remaining_url_length -=\
            len('/_apis/wit/workItems?ids=&fields=&asOf=2017-11-07T17%3A05%3A34.06699999999999999Z')
        fields = []
        fields_length_in_url = 0
        if query_result.columns:
            for field_ref in query_result.columns:
                fields.append(field_ref.reference_name)
                if fields_length_in_url > 0:
                    fields_length_in_url += 3  # add 3 for %2C delimiter
                fields_length_in_url += len(uri_quote(field_ref.reference_name))
                if fields_length_in_url > 800:
                    logger.info("Not retrieving all fields due to max url length.")
                    break
        remaining_url_length -= fields_length_in_url
        max_work_items = 1000
        work_items_batch_size = 200
        current_batch = []
        work_items = []
        work_item_url_length = 0
        for work_item_ref in query_result.work_items:
            if len(work_items) >= max_work_items:
                logger.info("Only retrieving the first %s work items.", max_work_items)
                break
            if work_item_url_length > 0:
                work_item_url_length += 3  # add 3 for %2C delimiter
            work_item_url_length += len(str(work_item_ref.id))
            current_batch.append(work_item_ref.id)

            if remaining_url_length - work_item_url_length <= 0 or len(current_batch) == work_items_batch_size:
                # url is near max length, go ahead and send first request for details.
                # url can go over by an id length because we have a safety buffer
                current_batched_items = client.get_work_items(ids=current_batch,
                                                              as_of=query_result.as_of,
                                                              fields=fields)
                for work_item in current_batched_items:
                    work_items.append(work_item)
                current_batch = []
                work_item_url_length = 0

        if current_batch:
            current_batched_items = client.get_work_items(ids=current_batch,
                                                          as_of=query_result.as_of,
                                                          fields=fields)
            for work_item in current_batched_items:
                work_items.append(work_item)
        # put items in the same order they appeared in the initial query results
        work_items = sorted(work_items, key=_get_sort_key_from_last_query_results)
        return work_items
    return None


def _get_sort_key_from_last_query_results(work_item):
    work_items = get_last_query_result().work_items
    i = 0
    num_items = len(work_items)
    while i < num_items:
        if work_items[i].id == work_item.id:
            return i
        i += 1
    # following lines should never be reached
    raise CLIError("Work Item {} was not found in the original query results.".format(work_item.id))


_last_query_result = {}
_LAST_QUERY_RESULT_KEY = 'value'


def get_last_query_result():
    return _last_query_result.get(_LAST_QUERY_RESULT_KEY, None)


def _open_work_item(work_item, organization):
    """Opens the work item in the default browser.
    :param work_item: The work item to open.
    :type work_item: :class:`<WorkItem> <v5_0.work-item-tracking.models.WorkItem>`
    """
    project = work_item.fields['System.TeamProject']
    url = organization.rstrip('/') + '/' + uri_quote(project) + '/_workitems?id='\
        + uri_quote(str(work_item.id))
    logger.debug('Opening web page: %s', url)
    webbrowser.open_new(url=url)


def _create_patch_operation(op, path, value):
    patch_operation = JsonPatchOperation()
    patch_operation.op = op
    patch_operation.path = path
    patch_operation.value = value
    return patch_operation


def _create_work_item_field_patch_operation(op, field, value):
    path = '/fields/{field}'.format(field=field)
    return _create_patch_operation(op=op, path=path, value=value)


def _resolve_identity_as_unique_user_id(identity_filter, organization):
    """Takes an identity name, email, alias, or id, and returns the unique_user_id.
    """
    if identity_filter.find(' ') > 0 or identity_filter.find('@') > 0:
        return identity_filter
    if identity_filter.lower() == ME:
        identity = get_current_identity(organization)
    else:
        # For alias
        identity = resolve_identity(identity_filter, organization)
    if identity is not None:
        return get_account_from_identity(identity)
    return None


_SYSTEM_FIELD_ARGS = {'System.Title': 'title',
                      'System.Description': 'description',
                      'System.AssignedTo': 'assigned-to',
                      'System.State': 'state',
                      'System.AreaPath': 'area-path',
                      'System.IterationPath': 'iteration-path',
                      'System.Reason': 'reason',
                      'System.History': 'history'}
