"""
This is a collection of capabilities for loading and transforming the objects in this repo.
"""

import copy
import glob
import gzip
import http.cookiejar
import os
from pathlib import Path
import requests

from tempfile import NamedTemporaryFile
import yaml


def project_path(directory=None):
    """Return the full root or sub-directory path in this project"""
    root = os.path.dirname(os.path.dirname(__file__))
    if directory:
        return os.path.join(root, directory)
    return root


def is_playground():
    """Return whether this project is the playground or not"""
    return project_path().endswith('events_playground')


def list_markdown_objects(path):
    """Return the names of files in a directory as the supported instantiations of that object"""
    for x in glob.glob(f'{path}/*.md', recursive=True):
        if 'README.md' not in x:
            yield os.path.basename(x).split('.')[0]


def load_object(path):
    """Load a YAML object by file path"""
    with open(path) as fp:
        # The default Loader is no longer safe: https://msg.pyyaml.org/load
        return yaml.load(fp, Loader=yaml.SafeLoader)


def load_yaml_objects(directory):
    results = {}
    for path in glob.glob(f'{directory}/*.yaml', recursive=True):
        results[path.split(f'{directory}/', 1)[1]] = load_object(path)
    return results


# The default dumper doesn't properly indent lists
# https://web.archive.org/web/20170903201521/https://pyyaml.org/ticket/64
class SafeIndentingDumper(yaml.SafeDumper):
    def increase_indent(self, flow=False, indentless=False):
        return super().increase_indent(flow, False)


def write_object(obj_type, obj):
    contents = yaml.dump(obj, Dumper=SafeIndentingDumper)
    with open(project_path(obj_type) + f'/{list(obj.keys())[0]}.yaml', 'w') as fp:
        fp.write(contents)


def objects_by_name(objs):
    """Given a list of objects each keyed by their name, return a unified dictionary"""
    # objs is from load_yaml_objects(X).values()
    # converts [{k: {<object_contents>}},...] to {k: {<obj_contents>}, ...}
    return {k: v for obj in objs for k, v in obj.items()}


def get_fields():
    return load_yaml_objects(project_path('fields'))


def get_groups():
    return load_yaml_objects(project_path('groups'))


def get_events():
    return load_yaml_objects(project_path('events'))


def get_deprecated():
    return load_object(project_path('scripts') + '/deprecated.yaml')['deprecated']


def list_transforms():
    return list(list_markdown_objects(project_path('transforms')))


def list_types():
    return list(list_markdown_objects(project_path('types')))


def list_expectations():
    return list(list_markdown_objects(project_path('expectations')))


def expand_group(g, groups):
    """Given a group return the list of all fields a group encompasses"""
    fields = {x for x in g.get('fields', [])}
    # groups are acyclic
    q = [x for x in g.get('groups', [])]
    while q:
        g = groups[q.pop()]
        for f in g.get('fields', []):
            fields.add(f)
        q.extend(g.get('groups', []))
    return sorted(fields)

def update_line(lines, prefix, content):
    """Search a line that start with the prefix and replace it with content"""
    for idx, line in enumerate(lines):
        if line.startswith(prefix):
            lines[idx] = content
            return True
    return False

def expand_event_fields(groups, fields, all_groups, all_fields):
    """Expand complete a deepcopied list for all groups and fields on an event"""
    if groups is None:
        groups = []
    groups += ['all_events_common_fields']
    if fields is None:
        fields = []
    results = set(fields)
    for g in groups:
        results |= set(expand_group(all_groups[g], all_groups))
    return {k: copy.deepcopy(v) for k, v in all_fields.items() if k in results}


def apply_overrides(overrides, fields):
    """Overwrite the fields with the provided overrides"""
    for override in overrides:
        field = fields[override['name']]
        if 'expectations' in override:
            field['expectations'] = override['expectations']
        if 'source' in override:
            if 'type' in field:
                field['type']['source'] = override['source']
            if 'transform' in field:
                field['transform']['source'] = override['source']
    return fields


def fixup_strings(fields):
    """Strings should have expectation on length instead of passing it forward"""
    for field in fields.values():
        if 'type' not in field or 'name' not in field['type'] or field['type']['name'] != 'string':
            continue
        e = {'name': 'value_lengths_to_be_between', 'min': 0, 'max': field['type']['length']}
        del field['type']['length']
        if 'expectations' not in field:
            field['expectations'] = []
        field['expectations'].append(e)
    return fields


def add_line(lines, start_marker, end_marker, line_to_add):
    """Adds a line in the correct place in the codeowners file"""
    searching = False
    for idx, line in enumerate(lines):
        if line.startswith(start_marker):
            searching = True
        elif searching and line.strip() and not line.startswith('#') and line > line_to_add:
            break
        elif searching and line.startswith(end_marker):
            # ideally we want a blank line before the end marker
            if not lines[idx-1].strip():
                idx -= 1
            break
    lines.insert(idx, line_to_add)


def update_codeowners(owner, events, fields, groups):
    """Update CODEOWNERS with new events, fields, and groups"""
    fname = project_path('.github') + '/CODEOWNERS'
    with open(fname, 'r') as fp:
        lines = [x for x in fp]

    if len([x for x in lines if owner in x]) == 0:
        print('It looks like you are adding a new owner, please add it to the top section.')

    for objs, obj_type in [(events, 'Event'), (fields, 'Field'), (groups, 'Group')]:
        start_marker, end_marker = f'# {obj_type} ownership.', f'# End {obj_type}'
        for obj in objs:
            file_path = f'/{obj_type.lower()}s/{obj}.yaml'
            file_path_with_owner = f'{file_path} {owner}\n'

            if not update_line(lines, file_path, file_path_with_owner):
                add_line(lines, start_marker, end_marker, file_path_with_owner)

    with open(fname, 'w') as fp:
        fp.write(''.join(lines))


def make_codegen_schema(events, groups, fields):
    """This resolves all overrides and builds the intermediate YAML representation for codegen
    Format:
    events:
      - name: event1
        description: something
        fields:
          - name: field1
            description: something
            type:
              - name: string
            expectations:
              - name: values_to_not_be_null
    """
    result_events = []
    # We want to sort all events and all fields per event
    for event_name, event_info in sorted(events.items()):
        event_fields = expand_event_fields(event_info.get('groups'), event_info.get('fields'),
            groups, fields)
        # remove internal fields
        event_fields = {k: v for k, v in event_fields.items() if v.get('internal') != True}
        event_fields = apply_overrides(event_info.get('overrides', []), event_fields)
        event_fields = fixup_strings(event_fields)
        event_obj = {'name': event_name, 'description': event_info['description'], 'fields': []}
        for field_name, field_info in event_fields.items():
            type = field_info['type']
            # int is outputted as long to the codegen long output
            # because longs are already supported in codegen.
            # If a user sends a value for an int field that is
            # outside the int boundary of  -2^31 and 2^31-1 inclusive
            # it'll just be changed to Null in tahoe.
            if type == 'int':
                type = 'long'
            event_obj['fields'].append({
                'name': field_info.get('source', field_name),
                'description': field_info['description'],
                'type': type,
                'expectations': field_info.get('expectations', [])
            })
        event_obj['fields'] = sorted(event_obj['fields'], key=lambda x: x['name'])
        result_events.append(event_obj)

    return {'events': result_events}


def write_codegen_schema(codegen_schema, filepath):
    with gzip.open(filepath, 'wt') as fp:
      fp.write(yaml.dump(codegen_schema, Dumper=SafeIndentingDumper))


def parse_lines_between(lines, start, end):
        results = []
        appending = False
        for line in lines:
            line = line.strip()
            if line.startswith(start):
                appending = True
            if line.startswith(end):
                appending = False
            if line and appending and not line.startswith('#'):
                results.append(line)
        return results


def create_midway_auth_session():
    """This is needed because the cookiejar lib cannot by default read the cookie files written
       by midway, because some cookies start with #HttpOnly_ and look like a comment.
    """
    cookie_file = f'{str(Path.home())}/.midway/cookie'
    with NamedTemporaryFile(mode='w+', delete=False) as cjf:
        try:
            with open(cookie_file, 'r') as ocf:
                for line in ocf:
                    cjf.write(line[10:] if line.startswith('#HttpOnly_') else line)
                cjf.seek(0)
        except FileNotFoundError as e:
            print(f'cookie file {cookie_file} does not exist, are you sure you\'ve run \'mwinit --aea\'?')
    cookie_jar = http.cookiejar.MozillaCookieJar(
                    filename=cjf.name)
    cookie_jar.load()
    session = requests.Session()
    session.cookies = cookie_jar
    return session
