import csv
import sys

import googleapiclient

import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam import utils
from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer

def _get_device_customerid():
  customer = GC_Values[GC_CUSTOMER_ID]
  if customer.startswith('C'):
    customer = customer[1:]
  return f'customers/{customer}'

def create():
    ci = gapi_cloudidentity.build_dwd()
    customer = _get_device_customerid()
    device_types = gapi.get_enum_values_minus_unspecified(
        ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
    body = {'deviceType': '', 'serialNumber': ''}
    i = 3
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if myarg == 'serialnumber':
            body['serialNumber'] = sys.argv[i+1]
            i += 2
        elif myarg == 'devicetype':
            body['deviceType'] = sys.argv[i+1].upper()
            if body['deviceType'] not in device_types:
                controlflow.expected_argument_exit('device_type',
                                                   ', '.join(device_types),
                                                   sys.argv[i+1])
            i += 2
        elif myarg in {'assettag', 'assetid'}:
            body['assetTag'] = sys.argv[i+1]
            i += 2
        else:
            controlflow.invalid_argument_exit(sys.argv[i], 'gam create device')
    if not body['serialNumber'] or not body['deviceType']:
        controlflow.system_error_exit(
            3, 'serial_number and device_type are required arguments for "gam create device".')
    result = gapi.call(ci.devices(), 'create', customer=customer, body=body)
    print(f'Created device {result["response"]["name"]}')


def _parse_action(action):
    kwargs = {}
    i = 3
    name = sys.argv[i]
    if name == 'id':
        i += 1
        name = sys.argv[i]
    i += 1
    if not name.startswith('devices/'):
        name = f'devices/{name}'
    customer = _get_device_customerid()
    # bah, inconsistencies in API
    if action == 'delete':
        kwargs['customer'] = customer
    else:
        kwargs['body'] = {'customer': customer}
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if action == 'wipe' and myarg == 'removeresetlock':
            kwargs['body']['removeResetLock'] = True
            i += 1
        else:
            controlflow.invalid_argument_exit(sys.argv[i], f'gam {action} device')
    return name, kwargs


def info():
    ci = gapi_cloudidentity.build_dwd()
    customer = _get_device_customerid()
    _, name = _get_deviceuser_name()
    device = gapi.call(ci.devices(), 'get', name=name, customer=customer)
    device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
        'deviceUsers', parent=name, customer=customer)
    for device_user in device_users:
        parent = device_user['name']
        device_user['client_states'] = gapi.get_all_pages(
                  ci.devices().deviceUsers().clientStates(),
                  'list', 'clientStates', parent=parent, customer=customer)
    display.print_json(device)
    print('Device Users:')
    display.print_json(device_users)


def _generic_action(action, device_user=False):
    ci = gapi_cloudidentity.build_dwd()
    customer = _get_device_customerid()
    name, kwargs = _parse_action(action)
    if device_user:
        endpoint = ci.devices().deviceUsers()
    else:
        endpoint = ci.devices()
    op = gapi.call(endpoint, action, name=name, **kwargs)
    print(op)


def delete():
    _generic_action('delete')


def cancel_wipe():
    _generic_action('cancelWipe')


def wipe():
    _generic_action('wipe')


def approve_user():
    _generic_action('approve', True)


def block_user():
    _generic_action('block', True)


def cancel_wipe_user():
    _generic_action('cancelWipe', True)


def delete_user():
    _generic_action('delete', True)


def wipe_user():
    _generic_action('wipe', True)


def _get_deviceuser_name():
    i = 3
    name = sys.argv[i]
    if name == 'id':
        i += 1
        name = sys.argv[i]
    if not name.startswith('devices/'):
        name = f'devices/{name}'
    return (i+1, name)

def info_state():
    ci = gapi_cloudidentity.build_dwd()
    gapi_directory_customer.setTrueCustomerId()
    customer = _get_device_customerid()
    customer_id = customer[10:]
    client_id = f'{customer_id}-gam'
    i, deviceuser = _get_deviceuser_name()
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if myarg == 'clientid':
            client_id = f'{customer_id}-{sys.argv[i+1]}'
            i += 2
        else:
            controlflow.invalid_argument_exit(sys.argv[i], 'gam info deviceuserstate')
    name = f'{deviceuser}/clientStates/{client_id}'
    result = gapi.call(ci.devices().deviceUsers().clientStates(), 'get',
                       name=name, customer=customer)
    display.print_json(result)


def update_state():
    ci = gapi_cloudidentity.build_dwd()
    gapi_directory_customer.setTrueCustomerId()
    customer = _get_device_customerid()
    customer_id = customer[10:]
    client_id = f'{customer_id}-gam'
    body = {}
    i, deviceuser = _get_deviceuser_name()
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if myarg == 'clientid':
            client_id = f'{customer_id}-{sys.argv[i+1]}'
            i += 2
        elif myarg in ['assettag', 'assettags']:
            body['assetTags'] = gam.shlexSplitList(sys.argv[i+1])
            if body['assetTags'] == ['clear']:
                # TODO: this doesn't work to clear
                # existing values. Figure out why.
                body['assetTags'] = [None]
            i += 2
        elif myarg in ['compliantstate', 'compliancestate']:
            comp_states = gapi.get_enum_values_minus_unspecified(
              ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['complianceState']['enum'])
            body['complianceState'] = sys.argv[i+1].upper()
            if body['complianceState'] not in comp_states:
                controlflow.expected_argument_exit('compliant_state',
                                                   ', '.join(comp_states),
                                                   sys.argv[i+1])
            i += 2
        elif myarg == 'customid':
            body['customId'] = sys.argv[i+1]
            i += 2
        elif myarg == 'healthscore':
            health_scores = gapi.get_enum_values_minus_unspecified(
            ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['healthScore']['enum'])
            body['healthScore'] = sys.argv[i+1].upper()
            if body['healthScore'] == 'CLEAR':
                body['healthScore'] = None
            if body['healthScore'] and body['healthScore'] not in health_scores:
                controlflow.expected_argument_exit('health_score',
                                                   ', '.join(health_scores),
                                                   sys.argv[i+1])
            i += 2
        elif myarg == 'customvalue':
            allowed_types = ['bool', 'number', 'string']
            value_type = sys.argv[i+1].lower()
            if value_type not in allowed_types:
                controlflow.expected_argument_exit('custom_value',
                                                   ', '.join(allowed_types),
                                                   sys.argv[i+1])
            key = sys.argv[i+2]
            value = sys.argv[i+3]
            if value_type == 'bool':
                value = gam.getBoolean(value, key)
            elif value_type == 'number':
                value = int(value)
            body.setdefault('keyValuePairs', {})
            body['keyValuePairs'][key] = {f'{value_type}Value': value}
            i += 4
        elif myarg in ['managedstate']:
            managed_states = gapi.get_enum_values_minus_unspecified(
              ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['managed']['enum'])
            body['managed'] = sys.argv[i+1].upper()
            if body['managed'] == 'CLEAR':
                body['managed'] = None
            if body['managed'] and body['managed'] not in managed_states:
                controlflow.expected_argument_exit('managed_state',
                                                   ', '.join(managed_states),
                                                   sys.argv[i+1])
            i += 2
        elif myarg in ['scorereason']:
            body['scoreReason'] = sys.argv[i+1]
            if body['scoreReason'] == 'clear':
                body['scoreReason'] = None
            i += 2
        else:
            controlflow.invalid_argument_exit(sys.argv[i], 'gam update deviceuserstate')
    name = f'{deviceuser}/clientStates/{client_id}'
    updateMask = ','.join(body.keys())
    result = gapi.call(ci.devices().deviceUsers().clientStates(), 'patch',
                       name=name, customer=customer, updateMask=updateMask, body=body)
    display.print_json(result)


def print_():
    # This function is rather messy thanks to
    # https://github.com/GAM-team/GAM/issues/1534
    # I'd prefer to keep it all in this function for now but if:
    #  - we find other list() operations that also hit this bug OR
    #  - it looks like this issue is going to exist on Google's side
    #    for a long time.
    # I'll enterain some cleanup here to "functionalize" (yuck) all of this.
    ci = gapi_cloudidentity.build_dwd()
    customer = _get_device_customerid()
    parent = 'devices/-'
    device_filter = None
    get_device_users = True
    view = None
    orderByList = []
    # default sort order needed by our 1 hour bug workaround
    orderBy = 'create_time'
    titles = []
    csvRows = []
    todrive = False
    sortHeaders = False
    i = 3
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if myarg in ['filter', 'query']:
            device_filter = sys.argv[i+1]
            i += 2
        elif myarg == 'company':
            view = 'COMPANY_INVENTORY'
            i += 1
        elif myarg == 'personal':
            view = 'USER_ASSIGNED_DEVICES'
            i += 1
        elif myarg == 'nocompanydevices':
            view = 'USER_ASSIGNED_DEVICES'
            i += 1
        elif myarg == 'nopersonaldevices':
            view = 'COMPANY_INVENTORY'
            i += 1
        elif myarg == 'nodeviceusers':
            get_device_users = False
            i += 1
        elif myarg == 'todrive':
            todrive = True
            i += 1
        elif myarg == 'orderby':
            fieldName = sys.argv[i + 1].lower()
            i += 2
            if fieldName in DEVICE_ORDERBY_CHOICES_MAP:
                fieldName = DEVICE_ORDERBY_CHOICES_MAP[fieldName]
                orderBy = ''
                if i < len(sys.argv):
                    orderBy = sys.argv[i].lower()
                    if orderBy in SORTORDER_CHOICES_MAP:
                        orderBy = SORTORDER_CHOICES_MAP[orderBy]
                        i += 1
                if orderBy != 'DESCENDING':
                    orderByList.append(fieldName)
                else:
                    orderByList.append(f'{fieldName} desc')
            else:
                controlflow.expected_argument_exit(
                    'orderby', ', '.join(sorted(DEVICE_ORDERBY_CHOICES_MAP)),
                    fieldName)
        elif myarg == 'sortheaders':
            sortHeaders = True
            i += 1
        else:
            controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices')
    view_name_map = {
      None: 'Devices',
      'COMPANY_INVENTORY': 'Company Devices',
      'USER_ASSIGNED_DEVICES': 'Personal Devices',
      }
    if orderByList:
        orderBy = ','.join(orderByList)
    custom_device_filter = bool(device_filter)
    # we store the devices in a dict keyed by name which is a unique ID.
    # that way when we get duplicate devices we just overwrite the name
    # with the latest copy we saw.
    devices = {}
    page_message = gapi.got_total_items_msg(view_name_map[view], '...\n')
    pageToken = None
    newest_device_date = ''
    total_items = 0
    while True:
        try:
            a_page = gapi.call(ci.devices(),
                               'list',
                               customer=customer,
                               pageSize=100,
                               pageToken=pageToken,
                               filter=device_filter,
                               view=view,
                               orderBy=orderBy,
                               throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O])
        except googleapiclient.errors.HttpError:
            sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n') 
            if orderBy != 'create_time' or custom_device_filter:
                controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n')
            sys.stderr.write(f' attempting to work around the bug by filtering for devices created on or after the newest we\'ve seen ({newest_device_date})...')
            device_filter = f'register:{newest_device_date}..'
            pageToken = None
            continue
        for dev in a_page.get('devices', []):
            total_items += 1
            devices[dev['name']] = dev
            dev_date = dev.get('createTime', '')
            # remove the Z
            dev_date = dev_date[:-1]
            # remove microseconds
            dev_date = dev_date.split('.')[0]
            if dev_date > newest_device_date:
                newest_device_date = dev_date
        pageToken = a_page.get('nextPageToken')
        if not pageToken:
            break
        sys.stderr.write(page_message.replace('%%total_items%%', str(total_items)))
    if get_device_users:
        page_message = gapi.got_total_items_msg('Device Users', '...\n')
        pageToken = None
        newest_deviceuser_date = ''
        total_items = 0
        device_users = {}
        if not custom_device_filter:
            device_filter = None
        while True:
            try:
                a_page = gapi.call(ci.devices().deviceUsers(),
                                   'list',
                                   customer=customer,
                                   parent=parent,
                                   pageSize=20,
                                   orderBy=orderBy,
                                   filter=device_filter,
                                   pageToken=pageToken,
                                   throw_reasons=[gapi_errors.ErrorReason.FOUR_O_O])
            except googleapiclient.errors.HttpError:
                sys.stderr.write('WARNING: GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.\n')
                if orderBy != 'create_time' or custom_device_filter:
                    controlflow.system_error_exit(5, 'GAM workaround for this issue only works if filter and orderby arguments are not used.\n')
                sys.stderr.write(f' attempting to work around the bug by filtering for device users created on or after the newest we\'ve seen ({newest_deviceuser_date})...')
                device_filter = f'register:{newest_deviceuser_date}..'
                pageToken = None
                continue
            for device_user in a_page.get('deviceUsers', []):
                total_items += 1
                dev_date = device_user.get('createTime', '')
                # remove the Z
                dev_date = dev_date[:-1]
                # remove microseconds
                dev_date = dev_date.split('.')[0]
                if dev_date > newest_deviceuser_date:
                    newest_deviceuser_date = dev_date
                deviceuser_name = device_user['name']
                device_users[deviceuser_name] = device_user
            pageToken = a_page.get('nextPageToken')
            if not pageToken:
                break
            sys.stderr.write(page_message.replace('%%total_items%%', str(total_items)))
        for deviceuser_name, device_user in device_users.items():
            device_id = deviceuser_name.split('/')[1]
            device_name = f'devices/{device_id}'
            if 'users' not in devices[device_name]:
                devices[device_name]['users'] = []
            devices[device_name]['users'].append(device_user)
    for device in devices.values():
        device = utils.flatten_json(device)
        for a_key in device:
            if a_key not in titles:
                titles.append(a_key)
        csvRows.append(device)
    if sortHeaders:
        display.sort_csv_titles(['name',], titles)
    display.write_csv_file(csvRows, titles, 'Devices', todrive)


def sync():
    ci = gapi_cloudidentity.build_dwd()
    device_types = gapi.get_enum_values_minus_unspecified(
        ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
    customer = _get_device_customerid()
    device_filter = None
    csv_file = None
    serialnumber_column = 'serialNumber'
    devicetype_column = 'deviceType'
    static_devicetype = None
    assettag_column = None
    unassigned_missing_action = 'delete'
    assigned_missing_action = 'donothing'
    missing_actions = ['delete', 'wipe', 'donothing']
    i = 3
    while i < len(sys.argv):
        myarg = sys.argv[i].lower().replace('_', '')
        if myarg in ['filter', 'query']:
          device_filter = sys.argv[i+1]
          i += 2
        elif myarg == 'csvfile':
          csv_file = sys.argv[i+1]
          i += 2
        elif myarg == 'serialnumbercolumn':
          serialnumber_column = sys.argv[i+1]
          i += 2
        elif myarg == 'devicetypecolumn':
          devicetype_column = sys.argv[i+1]
          i += 2
        elif myarg == 'staticdevicetype':
          static_devicetype = sys.argv[i+1].upper()
          if static_devicetype not in device_types:
                controlflow.expected_argument_exit('device_type',
                                                   ', '.join(device_types),
                                                   sys.argv[i+1])
          i += 2
        elif myarg in {'assettagcolumn', 'assetidcolumn'}:
          assettag_column = sys.argv[i+1]
          i += 2
        elif myarg == 'unassignedmissingaction':
          unassigned_missing_action = sys.argv[i+1].lower().replace('_', '')
          if unassigned_missing_action not in missing_actions:
              controlflow.expected_argument_exit('unassigned_missing_action',
                                                   ', '.join(missing_actions),
                                                   sys.argv[i+1])
          i += 2
        elif myarg == 'assignedmissingaction':
          assigned_missing_action = sys.argv[i+1].lower().replace('_', '')
          if assigned_missing_action not in missing_actions:
              controlflow.expected_argument_exit('assigned_missing_action',
                                                   ', '.join(missing_actions),
                                                   sys.argv[i+1])
          i += 2
        else:
            controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices')
    if not csv_file:
        controlflow.system_error_exit(
            3, 'csvfile is a required argument for "gam sync devices".')
    f = fileutils.open_file(csv_file)
    input_file = csv.DictReader(f, restval='')
    if serialnumber_column not in input_file.fieldnames:
        controlflow.csv_field_error_exit(serialnumber_column, input_file.fieldnames)
    if not static_devicetype and devicetype_column not in input_file.fieldnames:
        controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
    if assettag_column and assettag_column not in input_file.fieldnames:
        controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
    local_devices = {}
    for row in input_file:
        # upper() is very important to comparison since Google
        # always return uppercase serials
        local_device = {'serialNumber': row[serialnumber_column].strip().upper()}
        if static_devicetype:
            local_device['deviceType'] = static_devicetype
        else:
            local_device['deviceType'] = row[devicetype_column].strip()
        sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
        if assettag_column:
            local_device['assetTag'] = row[assettag_column].strip()
            sndt += f"-{local_device['assetTag']}"
        local_devices[sndt] = local_device
    fileutils.close_file(f)
    page_message = gapi.got_total_items_msg('Company Devices', '...\n')
    device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
    if assettag_column:
       device_fields.append('assetTag')
    fields = f'nextPageToken,devices({",".join(device_fields)})'
    remote_devices = {}
    remote_device_map = {}
    result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
            customer=customer, page_message=page_message,
            pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
    for remote_device in result:
        sn = remote_device['serialNumber']
        last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
        name = remote_device.pop('name')
        sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
        if assettag_column:
            if 'assetTag' not in remote_device:
                remote_device['assetTag'] = ''
            sndt += f"-{remote_device['assetTag']}"
        remote_devices[sndt] = remote_device
        remote_device_map[sndt] = {'name': name}
        if last_sync == NEVER_TIME_NOMS:
            remote_device_map[sndt]['unassigned'] = True
    devices_to_add = []
    for sndt, device in iter(local_devices.items()):
      if sndt not in remote_devices:
        devices_to_add.append(device)
    missing_devices = []
    for sndt, device in iter(remote_devices.items()):
      if sndt not in local_devices:
        missing_devices.append(device)
    print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
    for add_device in devices_to_add:
        print(f'Creating {add_device["serialNumber"]}')
        try:
            result = gapi.call(ci.devices(), 'create', customer=customer,
              throw_reasons=[gapi_errors.ErrorReason.FOUR_O_NINE], body=add_device)
            print(f' created {result["response"]["deviceType"]} device {result["response"]["name"]} with serial {result["response"]["serialNumber"]}')
        except googleapiclient.errors.HttpError:
            print(f' {add_device["serialNumber"]} already exists')
    for missing_device in missing_devices:
        sn = missing_device['serialNumber']
        sndt = f"{sn}-{missing_device['deviceType']}"
        if assettag_column:
          sndt += f"-{missing_device['assetTag']}"
        name = remote_device_map[sndt]['name']
        unassigned = remote_device_map[sndt].get('unassigned')
        action = unassigned_missing_action if unassigned else assigned_missing_action
        if action == 'donothing':
            pass
        else:
            if action == 'delete':
                kwargs = {'customer': customer}
            else:
                kwargs = {'body': {'customer': customer}}
            gapi.call(ci.devices(), action,
                name=name, **kwargs)
            print(f'{action}d {sn}')
