#  IRIS Source Code
#  contact@dfir-iris.org
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 3 of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import logging as log
import os
import traceback
import urllib.parse

from flask import Blueprint
from flask import request
from flask_login import current_user
from werkzeug import Response
from werkzeug.utils import secure_filename

from app import db
from app.blueprints.rest.parsing import parse_comma_separated_identifiers
from app.blueprints.rest.endpoints import endpoint_deprecated
from app.datamgmt.alerts.alerts_db import get_alert_status_by_name
from app.datamgmt.case.case_db import get_case
from app.datamgmt.iris_engine.modules_db import get_pipelines_args_from_name
from app.datamgmt.iris_engine.modules_db import iris_module_exists
from app.datamgmt.manage.manage_cases_db import get_filtered_cases
from app.datamgmt.manage.manage_cases_db import close_case, map_alert_resolution_to_case_status
from app.datamgmt.manage.manage_cases_db import get_case_details_rt
from app.datamgmt.manage.manage_cases_db import list_cases_dict
from app.datamgmt.manage.manage_cases_db import reopen_case
from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.module_handler.module_handler import configure_module_on_init
from app.iris_engine.module_handler.module_handler import instantiate_module_from_name
from app.iris_engine.tasker.tasks import task_case_update
from app.iris_engine.utils.common import build_upload_path
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.models.authorization import Permissions
from app.schema.marshables import CaseSchema
from app.schema.marshables import CaseDetailsSchema
from app.util import add_obj_history_entry
from app.blueprints.access_controls import ac_requires_case_identifier
from app.blueprints.access_controls import ac_api_requires
from app.blueprints.access_controls import ac_api_return_access_denied
from app.blueprints.responses import response_error
from app.blueprints.responses import response_success
from app.blueprints.rest.parsing import parse_pagination_parameters
from app.business.cases import cases_delete
from app.business.cases import cases_update
from app.business.cases import cases_create
from app.business.errors import BusinessProcessingError

manage_cases_rest_blueprint = Blueprint('manage_case_rest', __name__)


@manage_cases_rest_blueprint.route('/manage/cases/<int:identifier>', methods=['GET'])
@endpoint_deprecated('GET', '/api/v2/cases/<int:identifier>')
@ac_api_requires()
def get_case_api(identifier):
    if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=identifier)

    res = get_case_details_rt(identifier)
    if res:
        return response_success(data=res)

    return response_error(f'Case ID {identifier} not found')


@manage_cases_rest_blueprint.route('/manage/cases/filter', methods=['GET'])
@ac_api_requires()
def manage_case_filter() -> Response:

    pagination_parameters = parse_pagination_parameters(request)

    case_ids_str = request.args.get('case_ids', None, type=str)

    if case_ids_str:
        try:
            case_ids_str = parse_comma_separated_identifiers(case_ids_str)

        except ValueError:
            return response_error('Invalid case id')

    case_customer_id = request.args.get('case_customer_id', None, type=str)
    case_name = request.args.get('case_name', None, type=str)
    case_description = request.args.get('case_description', None, type=str)
    case_classification_id = request.args.get('case_classification_id', None, type=int)
    case_owner_id = request.args.get('case_owner_id', None, type=int)
    case_opening_user_id = request.args.get('case_opening_user_id', None, type=int)
    case_severity_id = request.args.get('case_severity_id', None, type=int)
    case_state_id = request.args.get('case_state_id', None, type=int)
    case_soc_id = request.args.get('case_soc_id', None, type=str)
    start_open_date = request.args.get('start_open_date', None, type=str)
    end_open_date = request.args.get('end_open_date', None, type=str)
    draw = request.args.get('draw', 1, type=int)
    search_value = request.args.get('search[value]', type=str)  # Get the search value from the request

    if type(draw) is not int:
        draw = 1

    filtered_cases = get_filtered_cases(
        current_user.id,
        pagination_parameters,
        case_ids=case_ids_str,
        case_customer_id=case_customer_id,
        case_name=case_name,
        case_description=case_description,
        case_classification_id=case_classification_id,
        case_owner_id=case_owner_id,
        case_opening_user_id=case_opening_user_id,
        case_severity_id=case_severity_id,
        case_state_id=case_state_id,
        case_soc_id=case_soc_id,
        start_open_date=start_open_date,
        end_open_date=end_open_date,
        search_value=search_value
    )
    if filtered_cases is None:
        return response_error('Filtering error')

    cases = {
        'total': filtered_cases.total,
        'cases': CaseDetailsSchema().dump(filtered_cases.items, many=True),
        'last_page': filtered_cases.pages,
        'current_page': filtered_cases.page,
        'next_page': filtered_cases.next_num if filtered_cases.has_next else None,
        'draw': draw
    }

    return response_success(data=cases)


@manage_cases_rest_blueprint.route('/manage/cases/delete/<int:identifier>', methods=['POST'])
@endpoint_deprecated('DELETE', '/api/v2/cases/<int:identifier>')
@ac_api_requires(Permissions.standard_user)
def api_delete_case(identifier):
    if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=identifier)

    try:
        cases_delete(identifier)
        return response_success('Case successfully deleted')
    except BusinessProcessingError as e:
        return response_error(e.get_message())


@manage_cases_rest_blueprint.route('/manage/cases/reopen/<int:identifier>', methods=['POST'])
@ac_api_requires(Permissions.standard_user)
def api_reopen_case(identifier):
    if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=identifier)

    if not identifier:
        return response_error("No case ID provided")

    case = get_case(identifier)
    if not case:
        return response_error("Tried to reopen an non-existing case")

    res = reopen_case(identifier)
    if not res:
        return response_error("Tried to reopen an non-existing case")

    # Reopen the related alerts
    if case.alerts:
        merged_status = get_alert_status_by_name('Merged')
        for alert in case.alerts:
            if alert.alert_status_id != merged_status.status_id:
                alert.alert_status_id = merged_status.status_id
                track_activity(f"alert ID {alert.alert_id} status updated to merged due to case #{identifier} being reopen",
                               caseid=identifier, ctx_less=False)

                db.session.add(alert)

    case = call_modules_hook('on_postload_case_update', data=case, caseid=identifier)

    add_obj_history_entry(case, 'case reopen')
    track_activity("reopen case ID {}".format(identifier), caseid=identifier)
    case_schema = CaseSchema()

    return response_success("Case reopen successfully", data=case_schema.dump(res))


@manage_cases_rest_blueprint.route('/manage/cases/close/<int:identifier>', methods=['POST'])
@ac_api_requires(Permissions.standard_user)
def api_case_close(identifier):
    if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=identifier)

    if not identifier:
        return response_error("No case ID provided")

    case = get_case(identifier)
    if not case:
        return response_error("Tried to close an non-existing case")

    res = close_case(identifier)
    if not res:
        return response_error("Tried to close an non-existing case")

    # Close the related alerts
    if case.alerts:
        close_status = get_alert_status_by_name('Closed')
        case_status_id_mapped = map_alert_resolution_to_case_status(case.status_id)

        for alert in case.alerts:
            if alert.alert_status_id != close_status.status_id:
                alert.alert_status_id = close_status.status_id
                alert = call_modules_hook('on_postload_alert_update', data=alert, caseid=identifier)

            if alert.alert_resolution_status_id != case_status_id_mapped:
                alert.alert_resolution_status_id = case_status_id_mapped
                alert = call_modules_hook('on_postload_alert_resolution_update', data=alert, caseid=identifier)

                track_activity(f"closing alert ID {alert.alert_id} due to case #{identifier} being closed",
                               caseid=identifier, ctx_less=False)

                db.session.add(alert)

    case = call_modules_hook('on_postload_case_update', data=case, caseid=identifier)

    add_obj_history_entry(case, 'case closed')
    track_activity("closed case ID {}".format(identifier), caseid=identifier, ctx_less=False)
    case_schema = CaseSchema()

    return response_success("Case closed successfully", data=case_schema.dump(res))


@manage_cases_rest_blueprint.route('/manage/cases/add', methods=['POST'])
@endpoint_deprecated('POST', '/api/v2/cases')
@ac_api_requires(Permissions.standard_user)
def api_add_case():
    case_schema = CaseSchema()

    try:
        case = cases_create(request.get_json())
        return response_success('Case created', data=case_schema.dump(case))
    except BusinessProcessingError as e:
        return response_error(e.get_message(), data=e.get_data())


@manage_cases_rest_blueprint.route('/manage/cases/list', methods=['GET'])
@ac_api_requires(Permissions.standard_user)
def api_list_case():
    data = list_cases_dict(current_user.id)

    return response_success("", data=data)


@manage_cases_rest_blueprint.route('/manage/cases/update/<int:identifier>', methods=['POST'])
@endpoint_deprecated('PUT', '/api/v2/cases/<int:identifier>')
@ac_api_requires(Permissions.standard_user)
def update_case_info(identifier):
    if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=identifier)

    case_schema = CaseSchema()
    try:
        case, msg = cases_update(identifier, request.get_json())
        return response_success(msg, data=case_schema.dump(case))
    except BusinessProcessingError as e:
        return response_error(e.get_message(), data=e.get_data())


@manage_cases_rest_blueprint.route('/manage/cases/trigger-pipeline', methods=['POST'])
@ac_api_requires(Permissions.standard_user)
@ac_requires_case_identifier()
def update_case_files(caseid):
    if not ac_fast_check_current_user_has_case_access(caseid, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=caseid)

    # case update request. The files should have already arrived with the request upload_files
    try:
        # Create the update task
        jsdata = request.get_json()
        if not jsdata:
            return response_error('Not a JSON content')

        pipeline = jsdata.get('pipeline')

        try:
            pipeline_mod = pipeline.split("-")[0]
            pipeline_name = pipeline.split("-")[1]
        except Exception as e:
            log.error(e.__str__())
            return response_error('Malformed request')

        ppl_config = get_pipelines_args_from_name(pipeline_mod)
        if not ppl_config:
            return response_error('Malformed request')

        pl_args = ppl_config['pipeline_args']
        pipeline_args = {}
        for argi in pl_args:

            arg = argi[0]
            fetch_arg = jsdata.get('args_' + arg)

            if argi[1] == 'required' and (not fetch_arg or fetch_arg == ""):
                return response_error("Required arguments are not set")

            if fetch_arg:
                pipeline_args[arg] = fetch_arg

            else:
                pipeline_args[arg] = None

        status = task_case_update(
            module=pipeline_mod,
            pipeline=pipeline_name,
            pipeline_args=pipeline_args,
            caseid=caseid)

        if status.is_success():
            # The job has been created, so return. The progress can be followed on the dashboard
            return response_success("Case task created")
        # We got some errors and cannot continue
        return response_error(status.get_message(), data=status.get_data())

    except Exception as _:
        traceback.print_exc()
        return response_error('Fail to update case', data=traceback.print_exc())


@manage_cases_rest_blueprint.route('/manage/cases/upload_files', methods=['POST'])
@ac_api_requires(Permissions.standard_user)
@ac_requires_case_identifier()
def manage_cases_uploadfiles(caseid):
    """
    Handles the entire the case management, i.e creation, update, list and files imports
    :param path: Path within the URL
    :return: Depends on the path, either a page a JSON
    """
    if not ac_fast_check_current_user_has_case_access(caseid, [CaseAccessLevel.full_access]):
        return ac_api_return_access_denied(caseid=caseid)

    # Files uploads of a case. Get the files, create the folder tree
    # The request "add" will start the check + import of the files.
    f = request.files.get('file')

    is_update = request.form.get('is_update', type=str)
    pipeline = request.form.get('pipeline', '', type=str)

    try:
        pipeline_mod = pipeline.split("-")[0]
    except Exception as e:
        log.error(e.__str__())
        return response_error('Malformed request')

    if not iris_module_exists(pipeline_mod):
        return response_error('Missing pipeline')

    mod, _ = instantiate_module_from_name(pipeline_mod)
    status = configure_module_on_init(mod)
    if status.is_failure():
        return response_error("Path for upload {} is not built ! Unreachable pipeline".format(
            os.path.join(f.filename)))

    case_customer = None
    case_name = None

    if is_update == "true":
        case = get_case(caseid)

        if case:
            case_name = case.name
            case_customer = case.client.name

    else:
        case_name = urllib.parse.quote(request.form.get('case_name', '', type=str), safe='')
        case_customer = request.form.get('case_customer', type=str)

    fpath = build_upload_path(case_customer=case_customer,
                              case_name=urllib.parse.unquote(case_name),
                              module=pipeline_mod,
                              create=is_update
                              )

    f.filename = secure_filename(f.filename)
    status = mod.pipeline_files_upload(fpath, f, case_customer, case_name, is_update)

    if status.is_success():
        return response_success(status.get_message())

    return response_error(status.get_message())
