import json
import os
import subprocess
import time
from multiprocessing import Process
import flask_swagger_ui
import signal
import requests
import sys
import ssl
import logging

from flask import Flask, request, jsonify, Response, render_template
from flask_restful import Api, reqparse
from common.context import api_function
from threading import Thread
from urllib.parse import urljoin

from lib.generate_cert import GenerateCert
from testapps import lb_least_conn
from prometheus_flask_exporter import PrometheusMetrics
import products.faas
import products.paas.app_runner
import socket
import urllib3
import re

# Set global logging format and level for the entire app
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] %(name)s: %(message)s'
)

app = Flask(__name__)
app.url_map.strict_slashes = False

# Initialize extensions
PrometheusMetrics(app)
api = Api(app)
api_function.set_swagger_ui(app, flask_swagger_ui)

# Register blueprints
app.register_blueprint(products.paas.app_runner.bp)

# Server ports configuration
http_svr_port_explorer = '10346'
http_svr_port_least_conn_1 = '8080'
http_svr_port_least_conn_2 = '9080'

# Process variables
process_least_conn_1 = None
process_least_conn_2 = None


# Add API route define with api path & method
@app.route('/', methods=['GET'])
def root():
    return api_function.render_markdown(request, 'Home.md')


@app.route('/test', methods=['GET'])
def ping_test():
    return api_function.make_common_response(request=request)


@app.route('/monitor/l7check', methods=['GET'])
def lb_health_check():
    return api_function.make_common_response(request=request)


@app.route('/status/<int:code>', methods=['GET'])
def status_code_test(code):
    """Return a response with the specified HTTP status code (100-599)."""
    if code < 100 or code > 599:
        return api_function.make_common_response(
            res_msg={'msg': 'Bad Request, status code must be between 100 and 599'},
            request=request,
            res_code=400
        )
    return api_function.make_common_response(
        res_msg={'msg': f'Status {code}'},
        request=request,
        res_code=code
    )


@app.route('/delay/<int:ms>', methods=['GET'])
def delay_test(ms):
    """Delay response by the specified milliseconds (max 60000ms)."""
    if ms < 0 or ms > 60000:
        return api_function.make_common_response(
            res_msg={'msg': 'Bad Request, delay must be between 0 and 60000 ms'},
            request=request,
            res_code=400
        )
    time.sleep(ms / 1000.0)
    return api_function.make_common_response(
        res_msg={'msg': f'Delayed {ms} ms'},
        request=request
    )


@app.route('/status/<int:code>/delay/<int:ms>', methods=['GET'])
def status_code_with_delay_test(code, ms):
    """Return a response with the specified HTTP status code after a delay."""
    if code < 100 or code > 599:
        return api_function.make_common_response(
            res_msg={'msg': 'Bad Request, status code must be between 100 and 599'},
            request=request,
            res_code=400
        )
    if ms < 0 or ms > 60000:
        return api_function.make_common_response(
            res_msg={'msg': 'Bad Request, delay must be between 0 and 60000 ms'},
            request=request,
            res_code=400
        )
    time.sleep(ms / 1000.0)
    return api_function.make_common_response(
        res_msg={'msg': f'Status {code}, Delayed {ms} ms'},
        request=request,
        res_code=code
    )


@app.route('/run_cmd', methods=['POST'])
def run_cmd():
    return api_function.run_cmd(reqparse=reqparse, request=request)


@app.route('/explorer/', defaults={'path': ''}, strict_slashes=True)
@app.route('/explorer/<path:path>')
def explorer(path):
    http_svr_url = f'http://0.0.0.0:{http_svr_port_explorer}'
    # Logging
    api_function.make_common_response(request=request,
                                      res_msg={'msg': f'Forward to {http_svr_url}/{path}'})

    # Construct the target URL without the 'explorer' path
    target_url = urljoin(http_svr_url, path)

    response = requests.request(request.method, target_url, headers=request.headers, data=request.get_data())

    # Change the Content-Type header of text files returned 'application/octet-stream' to 'text/plain'
    if response.headers['Content-Type'] == 'application/octet-stream':
        try:
            response_content = response.content.decode('utf-8')
        except UnicodeDecodeError:
            response_content = response.content
        response.headers['Content-Type'] = 'text/plain'
    else:
        response_content = response.content

    # Return the SimpleHTTPServer's response to the original requester
    return response_content, response.status_code, response.headers.items()


@app.route("/webhook", methods=['POST'])
def webhook_test():
    # Get the JSON data from the request
    payload = request.json
    return api_function.make_common_response(res_msg={"msg": payload}, request=request)


@app.route("/run_iperf", methods=['POST'])
def iperf_test():
    return api_function.run_iperf(reqparse=reqparse, request=request)


@app.route("/athenz_access_token", methods=['POST'])
def get_athenz_access_token():
    return api_function.get_athenz_access_token(reqparse=reqparse, request=request)


# Set default 'bucket_name' and 'object_name' prepared bucket and object path
@app.route(
    "/test_fos/buckets/", defaults={'bucket_name': "cqa-test-bucket", 'object_name': "fos_test.txt"}, methods=["PUT"]
)
@app.route(
    "/test_fos/buckets/<string:bucket_name>/<string:object_name>", methods=["PUT"]
)
def put_test_fos(bucket_name, object_name):
    return api_function.put_test_fos(
        reqparse=reqparse,
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# Set default 'bucket_name' and 'object_name' prepared bucket and object path
@app.route(
    "/get_fos_object/", defaults={'bucket_name': "cqa-test-bucket", 'object_name': "fos_test.txt"}, methods=["GET"]
)
@app.route(
    "/get_fos_object/<string:bucket_name>/<string:object_name>", methods=["GET"]
)
def get_fos_object(bucket_name, object_name):
    return api_function.get_fos_object(
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# Set default 'bucket_name' prepared bucket
@app.route(
    "/test_fos_crud/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["POST"]
)
@app.route(
    "/test_fos_crud/<string:bucket_name>/<string:object_name>", methods=["POST"]
)
def test_fos_crud(bucket_name, object_name):
    return api_function.test_fos_crud(
        reqparse=reqparse,
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# FOS: Create(Upload) API
@app.route("/create_fos_object/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["PUT"])
@app.route('/create_fos_object/<string:bucket_name>/<string:object_name>', methods=['PUT'])
def create_object_route(bucket_name, object_name):
    return api_function.create_fos_object(
        reqparse=reqparse,
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# FOS: Delete API
@app.route("/delete_fos_object/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["DELETE"])
@app.route('/delete_fos_object/<string:bucket_name>/<string:object_name>', methods=['DELETE'])
def delete_object_route(bucket_name, object_name):
    return api_function.delete_fos_object(
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# FOS: Get List API
@app.route("/list_objects/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["GET"])
@app.route('/list_objects/<string:bucket_name>', methods=['GET'])
def list_object_route(bucket_name):
    object_list = api_function.get_list_object(request, bucket_name)
    return jsonify(object_list)


# FOS: Update Bucket API
@app.route("/update_bucket/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["POST"])
@app.route('/update_bucket/<string:bucket_name>/<string:action>', methods=['POST'])
def update_bucket_route(bucket_name, action):
    return api_function.update_bucket(request, bucket_name, action)


# FOS: Download Object API
@app.route("/download_objects/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["GET"])
@app.route("/download_objects/<string:bucket_name>/<string:object_name>", methods=["GET"])
def download_object_route(bucket_name, object_name):
    return api_function.download_object(
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# FOS: Write to Dual Environment (SSK_Prod + Prod) API
@app.route("/write_to_dual_env/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["POST"])
@app.route("/write_to_dual_env/<string:bucket_name>/<string:object_name>", methods=["POST"])
def write_to_dual_env_route(bucket_name, object_name):
    return api_function.write_to_dual_env(
        reqparse=reqparse,
        request=request,
        bucket_name=bucket_name,
        obj_name=object_name,
    )


# FOS: List from Dual Environment (SSK_Prod + Prod) API
@app.route("/list_from_dual_env/", defaults={'bucket_name': "cqa-test-bucket"}, methods=["GET"])
@app.route("/list_from_dual_env/<string:bucket_name>", methods=["GET"])
def list_from_dual_env_route(bucket_name):
    result = api_function.list_from_dual_env(request, bucket_name)
    return jsonify(result)


@app.route('/test_fos_ui/')
def fos_test_ui():
    return render_template('fos_test.html')


@app.route('/test_fos_crud_ui/')
def fos_crud_test_ui():
    return render_template('fos_test_crud.html')


@app.route('/test_fos_dual_env_ui/')
def fos_dual_env_test_ui():
    return render_template('fos_dual_env_test.html')


@app.route('/dbs_test_ui/')
def dbs_test_ui():
    return render_template('dbs_test.html')


@app.route('/test_mysql_dual_env_ui/')
def mysql_dual_env_test_ui():
    return render_template('mysql_dual_env_test.html')


@app.route('/function_test_ui/')
def function_test_ui():
    return render_template('function_test.html')


@app.route("/app_runner_test_ui/", methods=["GET"])
def app_runner_test_ui():
    return render_template("app_runner_test.html")


# DBS: Connect Database API
@app.route('/connect', methods=['POST'])
def connect_dbs():
    data = request.get_json()
    product = data.get('product')
    service = data.get('service')
    user = data.get('user')
    password = data.get('password')
    database = data.get('database')
    return api_function.connect_to_db(product, service, user, password, database)


# DBS: Execute Query API
@app.route('/execute_query', methods=['POST'])
def execute_query():
    data = request.get_json()
    product = data.get('product')
    query = data.get('query')
    database = data.get('database')
    return api_function.execute_query(product, query, database)

# DBS: Execute OpenSearch API Query
@app.route('/execute_opensearch_query', methods=['POST'])
def execute_opensearch_query():
    data = request.get_json()
    service = data.get('service')
    user = data.get('user')
    password = data.get('password')
    method = data.get('method')
    endpoint = data.get('endpoint')
    payload = data.get('payload')
    return api_function.execute_opensearch_api_query(service, user, password, method, endpoint, payload)

# MySQL Dual Environment: Connect to both MySQL databases API
@app.route('/connect_dual_mysql', methods=['POST'])
def connect_dual_mysql():
    result = api_function.connect_to_dual_mysql(request)
    return jsonify(result)

# MySQL Dual Environment: Execute query on both MySQL databases API
@app.route('/execute_dual_mysql_query', methods=['POST'])
def execute_dual_mysql_query():
    result = api_function.execute_dual_mysql_query(request)
    return jsonify(result)

# FEP: Request header and parse
@app.route('/headers', methods=['GET'])
def get_headers():
    headers_to_check = ['Flava-Client-IP', 'Signature', 'Signature-Input']
    headers_response = {}
    for header in headers_to_check:
        value = request.headers.get(header)
        if value:
            headers_response[header] = value
        else:
            headers_response[header] = f'{header} not found'

    return jsonify(headers_response)

# EGP: Call Egress proxy test api
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@app.route('/test_egp', methods=["POST"])
def test_call_egp():
    data = request.get_json()
    egp_endpoint = os.environ['EGP_ENDPOINT'] = data.get('egp_endpoint')
    egp_token = os.environ['EGP_TOKEN']  = data.get('egp_token')
    remote_endpoint = os.environ['REMOTE_ENDPOINT']  = data.get('remote_endpoint')

    # CHECK ENV Missing
    if not all([egp_endpoint, remote_endpoint, egp_token]):
        return jsonify({"error": "Missing environment variables"}), 500

    # SET Proxy Header
    proxy_headers = {
        "Authorization": f"Bearer {egp_token}"
    }

    proxy = urllib3.ProxyManager(
        egp_endpoint,
        proxy_headers=proxy_headers,#Set Auth header for Proxy
        cert_reqs='CERT_NONE'
    )

    print(f"{egp_endpoint}")

    reason = None
    try:
        # Call test connection through EGP proxy
        response = proxy.request('GET', remote_endpoint, timeout=120.0)
        # On Success: The remote endpoint was opened via Egress, so Proxy returns 200 and the destination code is response.status
        http_code = response.status
        proxy_code = 200

    except Exception as e:
        error_msg = str(e)
        # When an error occurs, extract the proxy code from the error message (e.g. "403 Forbidden")
        status_match = re.search(r'failed: (\d{3})', error_msg)
        reason = error_msg
        if status_match:
            proxy_code = int(status_match.group(1))
            http_code = 000  # The destination code is 0 because it was blocked by the proxy.
        else:
            # other errors set 500
            proxy_code = 500
            http_code = 000

    # Return response as like curl testing
    # If there is error reason, add error message for checking
    if reason:
        return jsonify(f"HTTP: {http_code}, Proxy: {proxy_code}, ErrMsg: {reason}")
    else:
        return jsonify(f"HTTP: {http_code}, Proxy: {proxy_code}")

@app.route('/egp_test_ui/')
def egp_test_ui():
    return render_template('egp_connection_test.html')


@app.route('/<path:file_path>')
def render_markdown(file_path):
    return api_function.render_markdown(request, file_path)


# Catch all Error's for error(include client errors) logging & response msg
@app.errorhandler(Exception)
def handle_error(error):
    error_code = getattr(error, 'code')
    error_msg = str(error)
    return api_function.make_common_response(res_msg={"msg": error_msg}, request=request, res_code=error_code)


# Set Swagger spec path
@app.route("/swagger.json")
def create_swagger_spec():
    swagger_spec = open("resources/swagger_api_spec.json")
    return jsonify(json.load(swagger_spec))


@app.route('/start_least_conn_processes', methods=['POST'])
def start_least_conn_processes():
    global process_least_conn_1, process_least_conn_2

    if process_least_conn_1 is None or not process_least_conn_1.is_alive():
        process_least_conn_1 = Process(target=lb_least_conn.run_app_least_conn_1)
        process_least_conn_1.start()

    if process_least_conn_2 is None or not process_least_conn_2.is_alive():
        process_least_conn_2 = Process(target=lb_least_conn.run_app_least_conn_2)
        process_least_conn_2.start()

    return "Least connection processes started", 200


# Handler for Function product with two arguments
def handler(req, context):
    request_dict = dict(req)
    method = request_dict.get("method", "").upper()
    path = request_dict.get("path", "")

    # Set up the Flask test request context
    with app.test_request_context(path=path, method=method):
        return products.faas.handler(request_dict, context)


# Handler Function for kill process
def kill_process(signum, frame):
    global process_least_conn_1, process_least_conn_2
    try:
        if sub_process:
            sub_process.kill()  # Attempt to kill the subprocess
            sub_process.wait()  # Wait for the subprocess to terminate

        if process_least_conn_1 is not None:
            process_least_conn_1.terminate()
            process_least_conn_1.join()
            process_least_conn_1 = None

        if process_least_conn_2 is not None:
            process_least_conn_2.terminate()
            process_least_conn_2.join()
            process_least_conn_2 = None

    except Exception as e:
        print(f"Error killing subprocess: {e}")
    finally:
        print("Shutting down the server.")
        sys.exit(0)


# Run cqa-test-app for http, https
def run_http(http_port=80):
    # If set specific http port by env var, set the port for app
    if os.getenv("CQA_TEST_APP_HTTP_PORT"):
        http_port = os.getenv("CQA_TEST_APP_HTTP_PORT")
    try:
        app.run(host='0.0.0.0', port=http_port)
    except Exception as e:
        print(f"CQA Test App HTTP server error: {e}")


def run_https(https_port=443):
    # If set specific https port by env var, set the port for app
    if os.getenv("CQA_TEST_APP_HTTPS_PORT"):
        https_port = os.getenv("CQA_TEST_APP_HTTPS_PORT")
    try:
        cert_generator = GenerateCert()
        cert_file, key_file = cert_generator.create_self_signed_cert()
        # Create an SSL context from generated cert, key files
        context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        context.load_cert_chain(certfile=cert_file, keyfile=key_file)
        # After create SSL context, remove cert, key files
        os.remove(cert_file)
        os.remove(key_file)
        # Run Https with generated SSL context
        app.run(host='0.0.0.0', port=https_port, ssl_context=context)
    except Exception as e:
        print(f"CQA Test App HTTPS server error: {e}")


# Start a UDP server which can receive a UDP protocol requests from the client
def start_udp_server():
    udp_ip = "0.0.0.0"
    udp_port = int(os.getenv("UDP_PORT", "8080"))
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    except socket.error as e:
        print(f"Socket creation is failed: {e}")
        sys.exit(1)
    try:
        sock.bind((udp_ip, udp_port))
        print(f"UDP server listening on port {udp_port}")
    except socket.error as e:
        print(f"{udp_ip}:{udp_port} Binding is failed - {e}")
        sock.close()
        sys.exit(1)

    while True:
        try:
            data, addr = sock.recvfrom(1024)
            message = data.decode('utf-8', errors='replace')
            print(f"Received message: {message} from {addr}")

            response_message = "Message received successfully\r\n".encode('utf-8')
            sock.sendto(response_message, addr)
        except Exception as e:
            print(f"Data receiving/sending is failed: {e}")


# Start cqa test app server
if __name__ == '__main__':
    # Run simpleHTTPServer
    sub_process = subprocess.Popen(['python3', '-m', 'http.server', http_svr_port_explorer])

    # Register signal for kill process
    signal.signal(signal.SIGTERM, kill_process)

    # Start HTTP cqa test app server (port=80)
    http_thread = Thread(target=run_http)
    http_thread.start()

    # Start HTTPS cqa test app server (port=443)
    https_thread = Thread(target=run_https)
    https_thread.start()

    # Start UDP server if UDP_TEST environment variable is set to "true" in any case
    if os.getenv("UDP_TEST", "false").lower() == "true":
        udp_thread = Thread(target=start_udp_server)
        udp_thread.daemon = True
        udp_thread.start()

    # Wait for threads to complete
    http_thread.join()
    https_thread.join()
