# from datetime import datetime
import base64
import gzip
import json
import logging
import os
import sys
import tempfile
import time
from urllib.parse import urljoin
from functools import cache
from logging.handlers import RotatingFileHandler
from pathlib import Path
# from functools import wraps  # , cache
import petl
import requests
from oaaclient.client import OAAClient, OAAClientError
from oaaclient.templates import IdPProviderType
from oaaclient.templates import OAAPropertyType
from oaa.hooks.preflight_checks import preflight

petl.config.failonerror = True

# BASE_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
BASE_DIR = Path(os.getcwd())


stdout_handler = logging.StreamHandler(stream=sys.stderr)
# Make the file_handler optional
# handlers = [file_handler, stdout_handler]
handlers = [stdout_handler]

if os.getenv('FILE_LOGGING', 'False').lower() == 'true':
    handlers.append(RotatingFileHandler(filename=BASE_DIR / 'output.log'))

LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO').upper()

logging.basicConfig(
    level=LOG_LEVEL,
    # format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s',  # noqa E501
    # format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',  # noqa E501
    format='[%(asctime)s] {%(name)s:%(lineno)d} %(levelname)s - %(message)s',  # noqa E501
    handlers=handlers
)
logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)
logging.getLogger('oaaclient.client').setLevel(logging.INFO)

logger = logging.getLogger(__name__)

# VEZA_URL = os.environ['VEZA_URL']
# VEZA_API_KEY = os.environ['VEZA_API_KEY']


# Required properties to create an employee record:
REQUIRED_VEZA_FIELDS = (
    'unique_id',
    'name',
    'employee_number',
    'first_name',
    'last_name',
    # ('is_active', 'employment_status'),
    'is_active',
    'employment_status',
)

REQUIRED_FIELDS = {
    'LocalUser': ['name', 'unique_id'],
    'LocalGroup': ['name', 'unique_id'],
    'Employee': REQUIRED_VEZA_FIELDS,
    'LocalRole': ['name', 'unique_id'],
    'Resource': ['name', 'unique_id'],
    # IdP
    'User': ['identity', 'name', 'email'],
    'Group': ['unique_id', 'name', 'group_type']
}


@cache
def get_oaa_client(runner=None):
    """TODO: Docstring for get_oaa_client.
    :returns: TODO

    """
    from oaa.settings import config
    # load the Veza URL and API key from the environment

    # if not config:
    #     env_file = None

    #     if runner:
    #         env_file = runner._env_file
    #     config = load_config(env_file=env_file)

    veza_url = str(config.VEZA_URL or os.getenv('VEZA_URL'))
    veza_api_key = str(config.VEZA_API_KEY or os.getenv('VEZA_API_KEY'))
    logger.info(f"Testing Veza credentials for {veza_url}")
    # import bpdb; bpdb.set_trace()  # noqa: E702
    try:
        veza_con = OAAClient(url=veza_url,
                             api_key=veza_api_key)
        logger.info("Connected to Veza tenant")
    except OAAClientError as e:
        logger.error("Unable to connect to Veza API")
        logger.error(e)
        exit(1)

    return veza_con


# def format_date(date_string: str) -> str:
#     # Date strings from the CSV may require some special handling
#     # See the below link for format code help to convert the string
#     #
#     https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes #      # noqa E501

#     date_obj = datetime.strptime(date_string, "%Y-%m-%d")
#     formatted = f"{date_obj.isoformat()}Z"

#     return formatted


def idp_provider_types():
    '''
    List IDP types

    This will give you a list of the valid IDP provider types
    '''

    return [p.name for p in IdPProviderType]


def oaa_property_types():
    '''
    List property types

    This will give you a list of the valid IDP provider types.
    '''

    return [p.name for p in OAAPropertyType]


def get_or_create_permission(provider, name, permissions=[], **kwargs):
    if name not in provider.custom_permissions:
        provider.add_custom_permission(name,
                                       permissions=permissions,
                                       **kwargs)

def get_status_of_dataprovider(datasource_id):
    api = get_oaa_client()
    ds = api.api_get(f'/api/v1/providers/datasources/{datasource_id}')

    # import bpdb; bpdb.set_trace()  # noqa: E702

    return ds.get('status')

class CustomDictForSerialization(dict):
    '''This simple gives a nice way of outputting objects for serialization by
    utilities like jq.
    '''
    def __str__(self):
        return json.dumps(self, indent=2)

@cache
def list_insight_points():
    """List insights points
    :returns: TODO

    """
    veza_con = get_oaa_client()
    insightpoints = veza_con.api_get('/api/private/dataPlanes')

    return CustomDictForSerialization(insightpoints)


def create_oaa_for_rest_hook(insight_point_id=None, hook_name='REST-Hook'):
    """This function will create an OAA integration. This integration's only
    purpose is to make a connection between an Insight Point and teh LCM
    workflows in Veza. 

    :hook_name: TODO
    :insight_point_id: TODO
    :returns: TODO

    """

    # change this to match the ID of the insight point you want to have available
    # for LCM rest api calls

    if not insight_point_id:
        logger.error('insight_point_id required')

        return

    if insight_point_id not in [i['id'] for i in list_insight_points()['data_planes']]:
        logger.error('insight_point_id %s not found', insight_point_id)

        return

    provider_list = list_providers()
    provider_names_list = [p[1] for p in provider_list]

    if hook_name in provider_names_list:
        logger.error('provider by that name %s already exists', hook_name)

        return

    options = {
        'provisioning': True,
        'external_lifecycle_management_type': 'SEND_REST_PAYLOAD',
        'data_plane_id': insight_point_id 
    }

    veza_con = get_oaa_client()
    provider = veza_con.create_provider(hook_name,
                                        'application',
                                        options=options)

    return provider

def get_or_create_data_source(datasource_name,
                              provider_id,
                              options={}):

    veza_con = get_oaa_client()
    data_source = veza_con.get_data_source(datasource_name,
                                           provider_id)
    created = False

    if data_source:
        logger.info("Found existing data_source")
    else:
        # NOTE Fixed the missing `f`
        logger.info(f"Creating Data Source {datasource_name}")
        data_source = veza_con.create_data_source(datasource_name,
                                                  provider_id,
                                                  options=options)
        created = True
    logger.info(f"DataSource: {data_source['name']} ({data_source['id']})")

    return data_source, created


def compress_file_to_gzip_base64(file_path):
    try:
        with open(file_path, 'rb') as file:
            file_contents = file.read()
            compressed_contents = gzip.compress(file_contents)
            base64_contents = base64.b64encode(compressed_contents).decode('utf-8')  # noqa E501

        return base64_contents
    except FileNotFoundError:
        print(f"Error: File not found at '{file_path}'")

        return None
    except Exception as e:
        print(f"Error: {e}")

        return None


def base64_file(filepath, compression=None):

    assert os.path.exists(filepath)

    if compression:
        return compress_file_to_gzip_base64(filepath)

    with open(filepath, 'rb') as binary_file:
        binary_file_data = binary_file.read()
        base64_encoded_data = base64.b64encode(binary_file_data).decode('utf-8')  # noqa E501

    return base64_encoded_data


def make_csv_upload_payload(provider_id,
                            datasource_id,
                            table,
                            column_mappings=None):

    payload = {}

    if column_mappings:
        columns_to_keep = map(lambda c: c['column_name'], column_mappings)
        table = table.cut(list(columns_to_keep))

    # with tempfile.NamedTemporaryFile('wb', suffix='.csv') as _tempfile:
    with tempfile.TemporaryDirectory() as tmpdirname:
        tempfile_name = str(Path(tmpdirname) / 'tempfile.csv')
        # table.tocsv(_tempfile.name)
        table.tocsv(tempfile_name)
        logger.debug(f'temp_file: {tempfile_name}')

        b64_contents = base64_file(tempfile_name)

        payload = {
          "id": provider_id,
          "data_source_id": datasource_id,
          "csv_data": b64_contents[:min(len(b64_contents), 50)],
          # "name": "test-upload-name",
          "type": "what-should-type-be",
          "compression_type": 0,
          "priority_push": True
        }

        # logger.debug('payload, %s', payload)

        payload["csv_data"] = b64_contents

        return payload


# TODO preflights might be different for the different runners. The ones
# defined here mostly apply to DB runners.


@preflight(require_config=False)
def check_configuration(runner, *args, preflight_conditions={}, **kwargs):
    config = preflight_conditions.get('config')

    if not config:
        return ['invalid configuration']

    print('✅ Configuration appears valid')

    return []

@preflight(require_config=True)
def check_sources(runner, config, *args, **kwargs):
    """Check that there is at least one source

    :runner: TODO
    :config: TODO
    :*args: TODO
    :**kwargs: TODO
    :returns: TODO

    """

    if not hasattr(config, 'sources'):
        return ['there are not sources in this config']

    return []



@preflight
def check_oaa_connection(runner, *args, **kwargs):
    # Check 3: Check OAA connection
    # -----------------------------------

    problems = []

    provider_list = list_providers()

    if not provider_list:
        problems.append('cannot connect to Veza. Check API credentials')
    else:
        print('✅ OAA/Veza connection appears to be healthy')

    return problems


def list_providers(include_details=False):
    veza_con = get_oaa_client()
    providers = veza_con.get_provider_list()

    if not include_details:
        return [(p['id'], p['name']) for p in providers]

    for provider in providers:
        get_provider_schema(provider['id'], provider)

    return providers


def get_provider_schema(provider_id, provider=None):
    veza_con = get_oaa_client()
    details = veza_con.get_provider_by_id(provider_id)
    # details['schema_definition_json'] = details['schema_definition_json']
    details['schema'] = json.loads(base64.b64decode(details['schema_definition_json']+'hello world'))

    if provider:
        provider.update(details)

    return details



def delete_provider(provider_id=None, provider_name=None):
    veza_con = get_oaa_client()
    provider = None

    if not provider_id:
        if provider_name:
            provider = veza_con.get_provider(provider_name)

            if provider:
                provider_id = provider['id']
        else:
            logger.error('provider_id must be provided')

            return
    else:
        provider = veza_con.get_provider_by_id(provider_id)

    if not provider:
        # let's make sure this provider exists before trying to delete it
        logger.error('I can\'t find a provider matching the information provided')

        return

    res = veza_con.delete_provider(provider_id)
    logger.info('res: %s', res)
    logger.info('provider %s[%s] deleted', provider['name'], provider_id)


class VezaDownloader:

    """Docstring for VezaDownloader. """

    def __init__(self, query_id=None):
        """TODO: to be defined1.

        :veza_url: TODO

        """
        self._query_id = query_id
        self._client = None
        self.job_id = None
        get_oaa_client()
        self.veza_url = os.environ['VEZA_URL'] 
        self._api_token = os.environ['VEZA_API_KEY']

    def _u(self, path):
        return urljoin(self.veza_url, path)

    @property
    def client(self):
        if not self._client:
            # self._client = get_oaa_client()
            self._client = requests.Session()
            self._client.headers.update({'Authorization': f'Bearer {self._api_token}'})

        return self._client

    def get_query_spec(self):
        '''Get the query spec, which is needed to start the report.
        Steps: 
        1. Get query spec
            - `https://{{veza_url}}/api/v1/assessments/queries/{{query_id}}`

        '''
        # response = self.client.api_get(f'/api/v1/assessments/queries/{self._query_id}')
        response = self.client.get(self._u(f'/api/v1/assessments/queries/{self._query_id}')).json()

        return response

    def start_export(self):
        '''Start the export, return the job id
        2. Initiate CSV Export
            - POST `https://{{veza_url}}/api/private/assessments/query_spec:export?format=CSV`
        '''
        payload = self.get_query_spec()['value']
        payload['include_nodes'] = True
        payload['no_relation'] = False
        payload['result_value_type'] = 'SOURCE_AND_DESTINATION_NODES'
        res = self.client.post(self._u('/api/private/assessments/query_spec:export'),
                               params={'format':'CSV'},
                               json=payload)

        self.job_id = res.json()['id']

        return self.job_id

    def wait_for_export_to_complete(self, sleep_seconds=5):
        '''Wait for the export to be complete. Check the status of the job_id until
        it's done.
        3. Wait for Results
        - `https://{{veza_url}}/api/private/exports/019a0dbf-6d27-7976-9432-50250fcb805c v`


        processing typically goes through the following stages:

        CREATED -> PROCESSING -> FINISHED
        '''
        done = False
        start_time = time.monotonic()

        while not done:
            res = self.client.get(self._u(f'/api/private/exports/{self.job_id}')).json()['value']

            done = res['state'].lower() == 'finished'
            # if res['state'].lower() != 'processing':
            #     import bpdb; bpdb.set_trace()  # noqa: E702
            logger.info("=" * 25)
            logger.info(f'job_id: {self.job_id}')
            logger.debug(f"waiting to check the job again: state {res['state']}")
            logger.info(f"state: {res['state']}")
            logger.debug(f"updated_at: {res['updated_at']}")
            logger.info(f"finished_row_count: {res['finished_row_count']}")
            logger.debug(f"time elapsed: {time.monotonic() - start_time}s")
            time.sleep(sleep_seconds)

        return True

    # def download_query(self, query_id=None):
    def download_query(self, job_id=None):
        '''Download the query.

        '''
        headers = {
            'Authorization': f'Bearer {self._api_token}'
        }
        url = self._u(f'/api/private/exports/{job_id or self.job_id}:download')
        res = self.client.get(url, stream=True, headers=headers)

        return res

    def get_stream(self):

        res = self.download_query()
        data = petl.MemorySource(res.content)
        t = petl.fromcsv(data)

        return t

def fetch_veza_query(query_id, output_file=None, do_stream=False):
    # veza_con = get_oaa_client()
    downloader = VezaDownloader(query_id)

    downloader.start_export()
    downloader.wait_for_export_to_complete()

    downloaded_stream = downloader.get_stream()

    if not output_file:

        if do_stream:
            return downloaded_stream

        petl.tocsv(source=None)

    else:
        if not os.path.exists(output_file):
            downloaded_stream.tocsv(output_file)
        else:
            logger.error('file already exiss')


full_func_list = locals()
FUNCS = {func:full_func_list[func] for func in filter(lambda f: callable(full_func_list[f]), full_func_list)}  # noqa E501
