# Copyright (C) 2019-2024 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import json
import os
import requests
import time

from flask import abort, jsonify, send_from_directory, redirect, Response
from jinja2 import Environment, PackageLoader, select_autoescape
from resultsdbpy.view.archive_view import ArchiveView
from resultsdbpy.view.ci_view import CIView
from resultsdbpy.view.commit_view import CommitView
from resultsdbpy.view.site_menu import SiteMenu
from resultsdbpy.view.suite_view import SuiteView
from werkzeug.exceptions import HTTPException, InternalServerError
from webkitflaskpy import AuthedBlueprint
from webkitflaskpy.util import AssertRequest, cache_for


class ViewRoutes(AuthedBlueprint):
    def __init__(
        self, model, controller,
        import_name=__name__,
        title='Results Database', auth_decorator=None,
        archive_routes=None,
        suite_types=None,
        default_architecture=None,
        tests_limits=None,
        suites_limits=None,
        commits_limits=None,
        dashboard_queries=None,
    ):
        super(ViewRoutes, self).__init__('view', import_name, url_prefix=None, auth_decorator=auth_decorator)
        self._cache = {}

        self.title = title
        self.environment = Environment(
            loader=PackageLoader(package_name='resultsdbpy.view', package_path='templates'),
            autoescape=select_autoescape(['html', 'xml']),
        )

        self.suite_types = suite_types or {}
        self.default_architecture = default_architecture
        self.tests_limits = tests_limits or dict(max=50000, default=5000)
        self.suites_limits = suites_limits or dict(max=10000, default=1000)
        self.commits_limits = commits_limits or dict(max=10000, default=1000)
        self.dashboard_queries = dashboard_queries or []

        # Protecting js and css with auth doesn't make sense
        self.add_url_rule('/library/<path:path>', 'library', self.library, authed=False, methods=('GET',))
        self.add_url_rule('/assets/<path:path>', 'assets', self.assets, authed=False, methods=('GET',))
        self.add_url_rule('/assets/js/constants.js', 'constants', self.constants, authed=False, methods=('GET',))

        self.site_menu = SiteMenu(title=self.title)

        self.commits = CommitView(
            environment=self.environment,
            commit_controller=controller.commit_controller,
            site_menu=self.site_menu,
        )
        self.suites = SuiteView(
            environment=self.environment,
            upload_controller=controller.upload_controller,
            suite_controller=controller.suite_controller,
            site_menu=self.site_menu,
        )
        self.ci = CIView(
            environment=self.environment,
            ci_controller=controller.ci_controller,
            site_menu=self.site_menu,
        )
        self.archive = ArchiveView(
            environment=self.environment,
            archive_controller=controller.archive_controller,
            site_menu=self.site_menu,
        )

        self.add_url_rule('/', 'main', self.suites.search, methods=('GET',))
        self.add_url_rule('/search', 'search', self.suites.search, methods=('GET',))

        self.add_url_rule('/documentation', 'documentation', self.documentation, methods=('GET',))

        self.add_url_rule('/commit', 'commit', self.commits.commit, methods=('GET',))
        self.add_url_rule('/commit/info', 'commit_info', self.commits.info, methods=('GET',))
        self.add_url_rule('/commit/previous', 'commit_previous', self.commits.previous, methods=('GET',))
        self.add_url_rule('/commit/next', 'commit_next', self.commits.next, methods=('GET',))
        self.add_url_rule('/commits', 'commits', self.commits.commits, methods=('GET',))

        self.add_url_rule('/suites', 'suites', self.suites.results, methods=('GET',))
        self.add_url_rule('/investigate', 'investigate', self.suites.investigate, methods=('GET',))

        self.add_url_rule('/urls/queue', 'urls-queue', self.ci.queue, methods=('GET',))
        self.add_url_rule('/urls/worker', 'urls-worker', self.ci.worker, methods=('GET',))
        self.add_url_rule('/urls/build', 'urls-build', self.ci.build, methods=('GET',))

        self.add_url_rule('/archive', 'archive-list', self.archive.extract, methods=('GET',))
        self.add_url_rule('/archive/<path:path>', 'archive', self.archive.extract, methods=('GET',))
        # Archive routes control which suites get linked to result archives and how those links appear in the UI.
        # This dictionary will be processed by archiveRouter.js, there are 3 active parameters respected:
        #     enabled: If false, no archive link will be generated for this suite
        #     path: The file in the archive to link to
        #     label: Defines the user-visible name of the archive link
        # Dictionaries have a defined ancestry, and will use the most specific parameters possible. The ancestry
        # arguments are:
        #     <suite>: Name of a suite (like layout-tests)
        #     <result>: One of the expected results, like FAIL, TEXT or IMAGE
        self.archive_routes = archive_routes or dict()
        self.add_url_rule('/archive-routes', 'archive-routes', cache_for(hours=24 * 7)(lambda: jsonify(self.archive_routes)), methods=('GET',))

        self.site_menu.add_endpoint('Main', self.name + '.main', parameters=['branch'])
        self.site_menu.add_endpoint('Suites', self.name + '.suites', parameters=['branch'])
        self.site_menu.add_endpoint('Investigate', self.name + '.investigate', parameters=['branch'])
        self.site_menu.add_endpoint('Documentation', self.name + '.documentation')
        self.site_menu.add_endpoint('Commits', self.name + '.commits', parameters=['branch'])

        self.register_error_handler(500, self.response_500)

    def assets(self, path):
        AssertRequest.no_query()
        AssertRequest.is_type()
        path_split = os.path.split(path)
        return send_from_directory(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static', *path_split[:-1]), path_split[-1])

    def cache_resource(self, url):
        # This avoids headaches with cross-origin scripts
        cached_result = self._cache.get(url, None)
        if cached_result and cached_result[0] + 60 * 60 * 24 > time.time():
            return cached_result[1]
        result = requests.get(url)
        if result.status_code != 200:
            abort(404, description=f'Failed to cache resource at {url}')
        self._cache[url] = (time.time(), Response(result.text, mimetype=result.headers['content-type']))
        return self._cache[url][1]

    def library(self, path):
        AssertRequest.no_query()
        AssertRequest.is_type()
        path_split = os.path.split(path)
        return send_from_directory(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static/library', *path_split[:-1]), path_split[-1])

    def response_500(self, error=InternalServerError()):
        if isinstance(error, HTTPException):
            return self.error(error=error)
        return self.error(error=InternalServerError())

    @SiteMenu.render_with_site_menu()
    def error(self, error=InternalServerError(), **kwargs):
        response = self.environment.get_template('error.html').render(
            title=f'{self.title}: {error.code}',
            name=error.name, description=error.description,
            **kwargs)
        return response, error.code

    @SiteMenu.render_with_site_menu()
    def documentation(self, **kwargs):
        return self.environment.get_template('documentation.html').render(
            title=f'{self.title}: Documentation',
            **kwargs)

    def constants(self):
        return Response(
            self.environment.get_template('constants.js').render(
                XcodeCloud=self.suite_types.get('XcodeCloud') or [],
                default_architecture=json.dumps(self.default_architecture) if self.default_architecture else 'null',
                tests_limits=json.dumps(self.tests_limits),
                suites_limits=json.dumps(self.suites_limits),
                commits_limits=json.dumps(self.commits_limits),
                dashboard_queries=json.dumps(self.dashboard_queries),
            ), mimetype='application/javascript',
        )
