import copy
import json
import logging
import os
import pprint
import subprocess
import tempfile, shutil
import time

from bson.objectid import ObjectId
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
from hashlib import md5
from mongoengine.base import ValidationError

from crits.backdoors.backdoor import Backdoor
from crits.campaigns.forms import CampaignForm
from crits.config.config import CRITsConfig
from crits.core import form_consts
from crits.core.class_mapper import class_from_value, class_from_id
from crits.core.crits_mongoengine import EmbeddedSource, EmbeddedCampaign
from crits.core.crits_mongoengine import json_handler, create_embedded_source
from crits.core.data_tools import convert_string_to_bool, validate_md5_checksum
from crits.core.data_tools import validate_sha1_checksum, validate_sha256_checksum
from crits.core.exceptions import ZipFileError
from crits.core.forms import DownloadFileForm
from crits.core.handlers import build_jtable, jtable_ajax_list, jtable_ajax_delete
from crits.core.handlers import csv_export
from crits.core.handsontable_tools import convert_handsontable_to_rows, parse_bulk_upload
from crits.core.mongo_tools import get_file
from crits.core.source_access import SourceAccess
from crits.core.user_tools import is_admin, user_sources, get_user_organization
from crits.core.user_tools import is_user_subscribed, is_user_favorite
from crits.notifications.handlers import remove_user_from_notification
from crits.objects.handlers import object_array_to_dict
from crits.objects.handlers import validate_and_add_new_handler_object
from crits.samples.forms import XORSearchForm, UnzipSampleForm, UploadFileForm
from crits.samples.sample import Sample
from crits.samples.yarahit import YaraHit
from crits.services.analysis_result import AnalysisResult
from crits.services.handlers import run_triage, get_supported_services
from crits.stats.handlers import generate_yara_hits

from crits.vocabulary.relationships import RelationshipTypes

logger = logging.getLogger(__name__)


def generate_sample_csv(request):
    """
    Generate a CSV file of the Sample information

    :param request: The request for this CSV.
    :type request: :class:`django.http.HttpRequest`
    :returns: :class:`django.http.HttpResponse`
    """

    response = csv_export(request, Sample)
    return response


def get_sample_details(sample_md5, analyst, format_=None):
    """
    Generate the data to render the Sample details template.

    :param sample_md5: The MD5 of the Sample to get details for.
    :type sample_md5: str
    :param analyst: The user requesting this information.
    :type analyst: str
    :param format_: The format of the details page.
    :type format_: str
    :returns: template (str), arguments (dict)
    """

    template = None
    sources = user_sources(analyst)
    sample = Sample.objects(md5=sample_md5,
                            source__name__in=sources).first()
    if not sample:
        return ('error.html', {'error': "File not yet available or you do not have access to view it."})
    sample.sanitize_sources(username=analyst)
    if format_:
        exclude = [
                    "source",
                    "relationships",
                    "schema_version",
                    "campaign",
                    "analysis",
                    "bucket_list",
                    "ticket",
                    "releasability",
                    "unsupported_attrs",
                    "status",
                    "objects",
                    "modified",
                    "analyst",
                    "_id"
                  ]
        if format_ == "yaml":
            data = sample.to_yaml(exclude)
            return "yaml", data
        if format_ == "json":
            data = sample.to_json(exclude)
            return "json", data

    if not sample:
        template = "error.html"
        args = {'error': "No sample found"}
    elif format_ == "text":
        template = "samples_detail_text.html"
        args = {'sample': sample}
    else:
        #create forms
        xor_search_form = XORSearchForm()
        campaign_form = CampaignForm()
        unzip_sample_form = UnzipSampleForm()
        download_form = DownloadFileForm(initial={"obj_type":'Sample',
                                                  "obj_id":sample.id,
                                                  "meta_format": "none"})

        # do we have the binary?
        if isinstance(sample.filedata.grid_id, ObjectId):
            binary_exists = 1
        else:
            binary_exists = 0

        sample.sanitize("%s" % analyst)

        # remove pending notifications for user
        remove_user_from_notification("%s" % analyst, sample.id, 'Sample')

        # subscription
        subscription = {
                'type': 'Sample',
                'id': sample.id,
                'subscribed': is_user_subscribed("%s" % analyst,
                                                'Sample',
                                                sample.id),
        }

        #objects
        objects = sample.sort_objects()

        #relationships
        relationships = sample.sort_relationships("%s" % analyst,
                                                meta=True)

        # relationship
        relationship = {
                'type': 'Sample',
                'value': sample.id
        }

        #comments
        comments = {'comments': sample.get_comments(),
                    'url_key': sample_md5}

        #screenshots
        screenshots = sample.get_screenshots(analyst)

        # favorites
        favorite = is_user_favorite("%s" % analyst, 'Sample', sample.id)

        # services
        service_list = get_supported_services('Sample')

        # analysis results
        service_results = sample.get_analysis_results()
        
        # template
        from crits.services.core import ServiceManager
        service_manager     = ServiceManager()
        tmp_service_results = []
        
        for result in service_results:
            result.template = service_manager.get_service_class(result.service_name).template
            tmp_service_results.append(result)
        
        service_results = tmp_service_results
        

        args = {'objects': objects,
                'relationships': relationships,
                'comments': comments,
                'relationship': relationship,
                'subscription': subscription,
                'sample': sample, 'sources': sources,
                'campaign_form': campaign_form,
                'download_form': download_form,
                'xor_search_form': xor_search_form,
                'unzip_sample_form': unzip_sample_form,
                'binary_exists': binary_exists,
                'favorite': favorite,
                'screenshots': screenshots,
                'service_list': service_list,
                'service_results': service_results}

    return template, args

def generate_sample_jtable(request, option):
    """
    Generate the jtable data for rendering in the list template.

    :param request: The request for this jtable.
    :type request: :class:`django.http.HttpRequest`
    :param option: Action to take.
    :type option: str of either 'jtlist', 'jtdelete', or 'inline'.
    :returns: :class:`django.http.HttpResponse`
    """

    obj_type = Sample
    type_ = "sample"
    mapper = obj_type._meta['jtable_opts']
    if option == "jtlist":
        # Sets display url
        details_url = mapper['details_url']
        details_url_key = mapper['details_url_key']
        fields = mapper['fields']
        response = jtable_ajax_list(obj_type, details_url, details_url_key,
                                    request, includes=fields)
        return HttpResponse(json.dumps(response,
                                       default=json_handler),
                            content_type="application/json")
    if option == "jtlist_by_org":
        # Sets display url
        details_url = mapper['details_url']
        details_url_key = mapper['details_url_key']
        get_values = request.GET.copy()
        get_values['source'] = get_user_organization("%s" % request.user.username)
        request.GET = get_values
        fields = mapper['fields']
        response = jtable_ajax_list(obj_type,details_url,details_url_key,
                                    request, includes=fields)
        return HttpResponse(json.dumps(response, default=json_handler),
                            content_type="application/json")
    if option == "jtdelete":
        response = {"Result": "ERROR"}
        if jtable_ajax_delete(obj_type,request):
            response = {"Result": "OK"}
        return HttpResponse(json.dumps(response,
                                       default=json_handler),
                            content_type="application/json")
    jtopts = {
        'title': "Samples",
        'default_sort': mapper['default_sort'],
        'listurl': reverse('crits.%ss.views.%ss_listing' %
                           (type_, type_), args=('jtlist',)),
        'deleteurl': reverse('crits.%ss.views.%ss_listing' %
                             (type_, type_), args=('jtdelete',)),
        'searchurl': reverse(mapper['searchurl']),
        'fields': mapper['jtopts_fields'],
        'hidden_fields': mapper['hidden_fields'],
        'linked_fields': mapper['linked_fields'],
        'details_link': mapper['details_link'],
        'no_sort': mapper['no_sort']
    }

    jtable = build_jtable(jtopts,request)
    jtable['toolbar'] = [
        {
            'tooltip': "'All Samples'",
            'text': "'All'",
            'click': "function () {$('#sample_listing').jtable('load', {'refresh': 'yes'});}",
            'cssClass': "'jtable-toolbar-center'",
        },
        {
            'tooltip': "'New Samples'",
            'text': "'New'",
            'click': "function () {$('#sample_listing').jtable('load', {'refresh': 'yes', 'status': 'New'});}",
            'cssClass': "'jtable-toolbar-center'",
        },
        {
            'tooltip': "'In Progress Samples'",
            'text': "'In Progress'",
            'click': "function () {$('#sample_listing').jtable('load', {'refresh': 'yes', 'status': 'In Progress'});}",
            'cssClass': "'jtable-toolbar-center'",
        },
        {
            'tooltip': "'Analyzed Samples'",
            'text': "'Analyzed'",
            'click': "function () {$('#sample_listing').jtable('load', {'refresh': 'yes', 'status': 'Analyzed'});}",
            'cssClass': "'jtable-toolbar-center'",
        },
        {
            'tooltip': "'Deprecated Samples'",
            'text': "'Deprecated'",
            'click': "function () {$('#sample_listing').jtable('load', {'refresh': 'yes', 'status': 'Deprecated'});}",
            'cssClass': "'jtable-toolbar-center'",
        },
        {
            'tooltip': "'Add Sample'",
            'text': "'Add Sample'",
            'click': "function () {$('#new-sample').click()}",
        },
    ]
    if option == "inline":
        return render_to_response("jtable.html",
                                  {'jtable': jtable,
                                   'jtid': '%s_listing' % type_,
                                   'button' : '%ss_tab' % type_},
                                  RequestContext(request))
    else:
        return render_to_response("%s_listing.html" % type_,
                                  {'jtable': jtable,
                                   'jtid': '%s_listing' % type_},
                                  RequestContext(request))

def generate_yarahit_jtable(request, option):
    """
    Generate the jtable data for rendering in the list template.

    :param request: The request for this jtable.
    :type request: :class:`django.http.HttpRequest`
    :param option: Action to take.
    :type option: str of either 'jtlist', 'jtdelete', or 'inline'.
    :returns: :class:`django.http.HttpResponse`
    """

    refresh = request.GET.get("refresh", "no")
    if refresh == "yes":
        generate_yara_hits()
    obj_type = YaraHit
    type_ = "yarahit"
    if option == "jtlist":
        # Sets display url
        details_url = 'crits.samples.views.samples_listing'
        details_url_key = "detectexact"
        response = jtable_ajax_list(obj_type,
                                    details_url,
                                    details_url_key,
                                    request)
        return HttpResponse(json.dumps(response,
                                       default=json_handler),
                            content_type="application/json")
    jtopts = {
        'title': "Yara Hits",
        'default_sort': "result ASC",
        'listurl': reverse('crits.samples.views.%ss_listing' % (type_,),
                           args=('jtlist',)),
        'deleteurl': "",
        'searchurl': reverse('crits.samples.views.%ss_listing' % (type_,)),
        'fields': ["result", "engine", "version", "sample_count","_id"],
        'hidden_fields': ["_id"],
        'linked_fields': []
    }
    jtable = build_jtable(jtopts,request)
    jtable['toolbar'] = [
        {
            'tooltip': "'Refresh Yara Hits'",
            'text': "'Refresh Stats'",
            'click': "function () {$.get('"+reverse('crits.samples.views.%ss_listing' % type_)+"', {'refresh': 'yes'}, function () { $('#yarahits_listing').jtable('reload');});}"
        },
    ]

    if option == "inline":
        return render_to_response("jtable.html",
                                  {'jtable': jtable,
                                   'jtid': '%ss_listing' % type_,
                                   'button' : '%ss_button' % type_},
                                  RequestContext(request))
    else:
        return render_to_response("%ss_listing.html" % type_,
                                  {'jtable': jtable,
                                   'jtid': '%ss_listing' % type_},
                                  RequestContext(request))

def get_filename(md5=None):
    """
    Get the filename of a sample by MD5.

    :param md5: The MD5 of the sample to get the filename of.
    :type md5: str
    :returns: None, str
    """

    if not md5:
        return None
    sample = Sample.objects(md5=md5).first()
    if not sample:
        return None
    return sample.filename

def get_md5_hash(oid=None):
    """
    Get the MD5 of a sample by ObjectId.

    :param oid: The ObjectId of the sample to get the MD5 of.
    :type oid: str
    :returns: None, str
    """

    if oid is None:
        return None
    else:
        sample = Sample.objects(id=oid).first()
        if not sample:
            return None
        return sample.md5

def delete_sample(sample_md5, username=None):
    """
    Delete a sample from CRITs.

    :param sample_md5: The MD5 of the sample to delete.
    :type sample_md5: str
    :param username: The user deleting this sample.
    :type username: str
    :returns: bool
    """

    if is_admin(username):
        sample = Sample.objects(md5=sample_md5).first()
        if sample:
            sample.delete(username=username)
            return True
        else:
            return False
    else:
        return False

def mail_sample(sample_md5, recips=None):
    """
    Mail a sample to a list of recipients.

    :param sample_md5: The MD5 of the sample to send.
    :type sample_md5: str
    :param recips: List of recipients.
    :type recips: list
    :returns: None, str
    """

    crits_config = CRITsConfig.objects().first()
    if recips is not None:
        sample = Sample.objects(md5=sample_md5).first()
        if not sample:
            return None
        try:
            send_mail('Details for %s' % sample_md5,
                      '%s' % pprint.pformat(sample.to_json()),
                      crits_config.crits_email,
                      recips,
                      fail_silently=False)
        except Exception as e:
            logger.error(e)
            return str(e.args)
    return None

def get_source_counts(analyst):
    """
    Get the sources for a user.

    :param analyst: The user to get sources for.
    :type analyst: str
    :returns: :class:`crits.core.crits_mongoengine.CritsQuerySet`
    """

    allowed = user_sources(analyst)
    sources = SourceAccess.objects(name__in=allowed)
    return sources

def get_yara_hits(version=None):
    """
    Get the yara hits in the database.

    :param version: The yara hit version to search for.
    :type version: str
    :returns: :class:`crits.core.crits_mongoengine.CritsQuerySet`
    """

    if version:
        hits = YaraHit.objects(version=version).order_by('+result')
    else:
        hits = YaraHit.objects().order_by('+result')
    return hits

def handle_unzip_file(md5, user=None, password=None):
    """
    Unzip a sample.

    :param md5: The MD5 of the sample to unzip.
    :type md5: str
    :param user: The user unzipping this sample.
    :type user: str
    :param password: Password to use to unzip the sample.
    :type password: str
    :returns: list
    :raises: ZipFileError, Exception
    """

    sample = class_from_value('Sample', md5)
    if not sample:
        return None
    data = sample.filedata.read()
    source = sample.source[0].name
    campaign = sample.campaign
    reference = ''
    return unzip_file(md5, user, password, data, source, method="Unzip Existing Sample",
                      reference=reference, campaign=campaign, related_md5=md5, )

def unzip_file(filename, user=None, password=None, data=None, source=None,
               method='Zip', reference='', campaign=None, confidence='low',
               related_md5=None, related_id=None, related_type='Sample',
               bucket_list=None, ticket=None, inherited_source=None,
               is_return_only_md5=True, backdoor_name=None,
               backdoor_version=None):
    """
    Unzip a file.

    :param filename: The name of the file to unzip.
    :type filename: str
    :param user: The user unzipping the file.
    :type user: str
    :param password: The password to use to unzip the file.
    :type password: str
    :param data: The filedata.
    :type data: str
    :param source: The name of the source that provided the data.
    :type source: str
    :param method: The source method to assign to the data.
    :type method: str
    :param reference: A reference to the data source.
    :type reference: str
    :param campaign: The campaign to attribute to the data.
    :type campaign: str
    :param confidence: The confidence level of the campaign attribution.
    :type confidence: str ('low', 'medium', 'high')
    :param related_md5: The MD5 of a related sample.