from flask import Flask, request
import time
import requests
import yaml
import random
import logging
from apscheduler.schedulers.background import BackgroundScheduler
import sys

# Define Flask frontend
app = Flask("frontend")

# Load Server configs from yaml
with open('config.yml', 'r') as file:
    config = yaml.load(file, Loader=yaml.FullLoader)

# Server addresses and list of replica addresses
FRONTEND_IP = config['frontend']
ORDER_IP1 = config['order1']
ORDER_IP2 = config['order2']
CATALOG_IP1 = config['catalog1']
CATALOG_IP2 = config['catalog2']

# List of possible replica servers
order_replica_list = [ORDER_IP1, ORDER_IP2]
catalog_replica_list = [CATALOG_IP1, CATALOG_IP2]

# List of available servers
available_order_list = []
available_catalog_list = []

# Local dictionary of book titles
book_titles = {
    1: "How to get a good grade in 677 in 20 minutes a day",
    2: "RPC for Dummies",
    3: "Xen and the Art of Surviving Graduate School",
    4: "Cooking for the Impatient Graduate Student",
    5: "How to finish Project 3 on time",
    6: "Why theory classes are so hard",
    7: "Spring in the Pioneer Valley"
}

# Define local cache
cache = {}

# Define do_caching, which if false disables caching
do_caching = True

# Initializers for performance logger
perf_logger = logging.getLogger("perf")
perf_logger.setLevel(logging.DEBUG)
perf_file_handler = logging.FileHandler('frontendperf.log')
perf_file_handler.setLevel(logging.DEBUG)
perf_logger.addHandler(perf_file_handler)


# Load balancing between replicas. Randomly choose server replica and return the location of one of the replica of the
# requested server type.
def get_server_location(replica):
    server_location = random.choice(replica)
    return server_location


# Heartbeat check for both the catalog and order replica
def check_heartbeat():
    logging.info("CHECKING HEARTBEAT!!!")
    # Check heartbeat for order replicas
    for order_replica in order_replica_list:
        try:
            response = requests.post("http://" + order_replica + "/ping/").json()
            if (response["status"]) and (order_replica not in available_order_list):
                available_order_list.append(order_replica)
                logging.info("Added " + order_replica + " to the list of available order replicas.")
        except requests.exceptions.ConnectionError:
            if order_replica in available_order_list:
                available_order_list.remove(order_replica)
                logging.info("Removed " + order_replica + " from the list of available order replicas.")

    # Check heartbeat for catalog replicas
    for catalog_replica in catalog_replica_list:
        try:
            response = requests.post("http://" + catalog_replica + "/ping/").json()
            if (response["status"]) and (catalog_replica not in available_catalog_list):
                available_catalog_list.append(catalog_replica)
                logging.info("Added " + catalog_replica + " to the list of available catalog replicas.")
        except requests.exceptions.ConnectionError:
            if catalog_replica in available_catalog_list:
                available_catalog_list.remove(catalog_replica)
                logging.info("Removed " + catalog_replica + " from the list of available catalog replicas.")
    return


# Invalidate a frontend cache. The parameters could be a topic or an item number.
@app.route("/invalidate/<key>", methods=["POST"])
def invalidate(key):
    if not do_caching:
        return "Caching not enabled"
    if key in cache:
        del cache[key]
        return "Entry " + key + " deleted."
    return "Entry " + key + " not in cache."


# Search for the requested topic
@app.route("/search/<topic>")
def search(topic: str) -> str:
    # Check cache if the topic is in cache
    if do_caching:
        if topic in cache:
            return cache[topic]

    logging.info(f"Searching for topic: {topic}")
    server_location = get_server_location(available_catalog_list)
    topic_query = {
        "topic": topic
    }

    try:
        search_start = time.perf_counter_ns()
        books = requests.post("http://" + server_location + '/query/', json=topic_query).json()
        search_elapsed = time.perf_counter_ns() - search_start
        perf_logger.info(f"search time: {search_elapsed}")

        # Parse the json of the search result
        search_result = []
        for book in books:
            search_result.extend(["Title: ", book, " "])
            search_result.extend(["ID: ", str(books[book]['item_number']), "\n"])

        final_result = "".join(search_result)
        if do_caching:
            cache[topic] = final_result

        # Return the search result in a string
        return final_result + "\n" + "Elapsed time (search): " + str(search_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Randomly pick another one to try again
        available_catalog_list.remove(server_location)
        print("Chosen catalog replica failed to respond. Trying another replica")
        return search(topic)


# Lookup the requested item number
@app.route("/lookup/<int:item_number>")
def lookup(item_number):
    # Check cache to see if item_number is in cache
    if do_caching:
        if str(item_number) in cache:
            return cache[str(item_number)]

    title = book_titles[int(item_number)]
    logging.info(f"Looking up item: {title}")
    server_location = get_server_location(available_catalog_list)
    item_query = {
        "item_number": item_number
    }

    try:
        lookup_start = time.perf_counter_ns()
        books = requests.post("http://" + server_location + '/query/', json=item_query).json()
        lookup_elapsed = time.perf_counter_ns() - lookup_start
        perf_logger.info(f"lookup time: {lookup_elapsed}")

        # Parse the lookup result
        lookup_result = []
        for book in books:
            lookup_result.extend(["Title: ", book, "\n"])
            lookup_result.extend(["Price: ", str(books[book]["price"]), "\n"])
            lookup_result.extend(["Stock: ", str(books[book]["stock"]), "\n"])

        final_result = "".join(lookup_result)
        if do_caching:
            cache[str(item_number)] = final_result

        # Return the lookup result in a string
        return final_result + "\n" + "Elapsed time (lookup): " + str(lookup_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Randomly pick another one to try again
        available_catalog_list.remove(server_location)
        print("Chosen catalog replica failed to respond. Trying another replica")
        return lookup(item_number)


# Buy the requested item number
@app.route("/buy/<item_number>")
def buy(item_number):
    title = book_titles[int(item_number)]
    server_location = get_server_location(available_order_list)

    logging.info(f"Attempting to buy item: {title}")
    frontend_buy_start = time.perf_counter_ns()

    try:
        response = requests.post("http://" + server_location + '/buy/' + item_number).json()
        frontend_buy_elapsed = time.perf_counter_ns() - frontend_buy_start

        perf_logger.info(f"frontend buy time: {frontend_buy_elapsed}")
        order_elapsed = response["elapsed_time"]
        perf_logger.info(f"order buy time: {order_elapsed}")

        # Use the 'status' boolean in json to check if the purchase was successful
        if response["status"]:
            logging.info("Success")
            return "Successfully purchased: " + str(title) + "\n" + \
                   "Elapsed time (buy, order server): " + str(response["elapsed_time"]) + \
                   "\n" + "Elapsed time (buy, frontend server): " + str(frontend_buy_elapsed)
        else:
            logging.error("Purchase Failed")
            return "Failed to purchase: " + str(title) + "\n" + \
                   "Elapsed time (buy, order server): " + str(response["elapsed_time"]) + \
                   "\n" + "Elapsed time (buy, frontend server): " + str(frontend_buy_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Remove replica from the available order replica list Randomly pick
        # another one to try again
        available_order_list.remove(server_location)
        print("Chosen order replica failed to respond. Trying another replica.")
        return buy(item_number)


if __name__ == "__main__":
    log_path = "logs/frontend.txt"
    open(log_path, "w").close()
    logging.basicConfig(filename=log_path, level=logging.DEBUG, format="%(asctime)s %(message)s")
    FRONTEND_PORT = config['frontend'].split(":")[-1]
    logging.info(f"Frontend server starting on port {FRONTEND_PORT}")

    # Check caching settings
    if len(sys.argv) > 1:
        do_cache_str = sys.argv[1].lower()
        if do_cache_str == "true" or do_cache_str == "t":
            do_caching = True
        elif do_cache_str == "false" or do_cache_str == "f":
            do_caching = False
    else:
        do_caching = True


    # Initially all servers are assumed to be up
    available_order_list = order_replica_list.copy()
    available_catalog_list = catalog_replica_list.copy()

    # Background scheduler that checks for heartbeat every 10 seconds
    scheduler = BackgroundScheduler()
    job = scheduler.add_job(check_heartbeat, 'interval', seconds=3)
    scheduler.start()

    app.run(host='0.0.0.0', port=FRONTEND_PORT)

from flask import Flask, request
import time
import requests
import yaml
import random
import logging
from apscheduler.schedulers.background import BackgroundScheduler
import sys

# Define Flask frontend
app = Flask("frontend")

# Load Server configs from yaml
with open('config.yml', 'r') as file:
    config = yaml.load(file, Loader=yaml.FullLoader)

# Server addresses and list of replica addresses
FRONTEND_IP = config['frontend']
ORDER_IP1 = config['order1']
ORDER_IP2 = config['order2']
CATALOG_IP1 = config['catalog1']
CATALOG_IP2 = config['catalog2']

# List of possible replica servers
order_replica_list = [ORDER_IP1, ORDER_IP2]
catalog_replica_list = [CATALOG_IP1, CATALOG_IP2]

# List of available servers
available_order_list = []
available_catalog_list = []

# Local dictionary of book titles
book_titles = {
    1: "How to get a good grade in 677 in 20 minutes a day",
    2: "RPC for Dummies",
    3: "Xen and the Art of Surviving Graduate School",
    4: "Cooking for the Impatient Graduate Student",
    5: "How to finish Project 3 on time",
    6: "Why theory classes are so hard",
    7: "Spring in the Pioneer Valley"
}

# Define local cache
cache = {}

# Define do_caching, which if false disables caching
do_caching = True

# Initializers for performance logger
perf_logger = logging.getLogger("perf")
perf_logger.setLevel(logging.DEBUG)
perf_file_handler = logging.FileHandler('frontendperf.log')
perf_file_handler.setLevel(logging.DEBUG)
perf_logger.addHandler(perf_file_handler)


# Load balancing between replicas. Randomly choose server replica and return the location of one of the replica of the
# requested server type.
def get_server_location(replica):
    server_location = random.choice(replica)
    return server_location


# Heartbeat check for both the catalog and order replica
def check_heartbeat():
    logging.info("CHECKING HEARTBEAT!!!")
    # Check heartbeat for order replicas
    for order_replica in order_replica_list:
        try:
            response = requests.post("http://" + order_replica + "/ping/").json()
            if (response["status"]) and (order_replica not in available_order_list):
                available_order_list.append(order_replica)
                logging.info("Added " + order_replica + " to the list of available order replicas.")
        except requests.exceptions.ConnectionError:
            if order_replica in available_order_list:
                available_order_list.remove(order_replica)
                logging.info("Removed " + order_replica + " from the list of available order replicas.")

    # Check heartbeat for catalog replicas
    for catalog_replica in catalog_replica_list:
        try:
            response = requests.post("http://" + catalog_replica + "/ping/").json()
            if (response["status"]) and (catalog_replica not in available_catalog_list):
                available_catalog_list.append(catalog_replica)
                logging.info("Added " + catalog_replica + " to the list of available catalog replicas.")
        except requests.exceptions.ConnectionError:
            if catalog_replica in available_catalog_list:
                available_catalog_list.remove(catalog_replica)
                logging.info("Removed " + catalog_replica + " from the list of available catalog replicas.")
    return


# Invalidate a frontend cache. The parameters could be a topic or an item number.
@app.route("/invalidate/<key>", methods=["POST"])
def invalidate(key):
    if not do_caching:
        return "Caching not enabled"
    if key in cache:
        del cache[key]
        return "Entry " + key + " deleted."
    return "Entry " + key + " not in cache."


# Search for the requested topic
@app.route("/search/<topic>")
def search(topic: str) -> str:
    # Check cache if the topic is in cache
    if do_caching:
        if topic in cache:
            return cache[topic]

    logging.info(f"Searching for topic: {topic}")
    server_location = get_server_location(available_catalog_list)
    topic_query = {
        "topic": topic
    }

    try:
        search_start = time.perf_counter_ns()
        books = requests.post("http://" + server_location + '/query/', json=topic_query).json()
        search_elapsed = time.perf_counter_ns() - search_start
        perf_logger.info(f"search time: {search_elapsed}")

        # Parse the json of the search result
        search_result = []
        for book in books:
            search_result.extend(["Title: ", book, " "])
            search_result.extend(["ID: ", str(books[book]['item_number']), "\n"])

        final_result = "".join(search_result)
        if do_caching:
            cache[topic] = final_result

        # Return the search result in a string
        return final_result + "\n" + "Elapsed time (search): " + str(search_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Randomly pick another one to try again
        available_catalog_list.remove(server_location)
        print("Chosen catalog replica failed to respond. Trying another replica")
        return search(topic)


# Lookup the requested item number
@app.route("/lookup/<int:item_number>")
def lookup(item_number):
    # Check cache to see if item_number is in cache
    if do_caching:
        if str(item_number) in cache:
            return cache[str(item_number)]

    title = book_titles[int(item_number)]
    logging.info(f"Looking up item: {title}")
    server_location = get_server_location(available_catalog_list)
    item_query = {
        "item_number": item_number
    }

    try:
        lookup_start = time.perf_counter_ns()
        books = requests.post("http://" + server_location + '/query/', json=item_query).json()
        lookup_elapsed = time.perf_counter_ns() - lookup_start
        perf_logger.info(f"lookup time: {lookup_elapsed}")

        # Parse the lookup result
        lookup_result = []
        for book in books:
            lookup_result.extend(["Title: ", book, "\n"])
            lookup_result.extend(["Price: ", str(books[book]["price"]), "\n"])
            lookup_result.extend(["Stock: ", str(books[book]["stock"]), "\n"])

        final_result = "".join(lookup_result)
        if do_caching:
            cache[str(item_number)] = final_result

        # Return the lookup result in a string
        return final_result + "\n" + "Elapsed time (lookup): " + str(lookup_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Randomly pick another one to try again
        available_catalog_list.remove(server_location)
        print("Chosen catalog replica failed to respond. Trying another replica")
        return lookup(item_number)


# Buy the requested item number
@app.route("/buy/<item_number>")
def buy(item_number):
    title = book_titles[int(item_number)]
    server_location = get_server_location(available_order_list)

    logging.info(f"Attempting to buy item: {title}")
    frontend_buy_start = time.perf_counter_ns()

    try:
        response = requests.post("http://" + server_location + '/buy/' + item_number).json()
        frontend_buy_elapsed = time.perf_counter_ns() - frontend_buy_start

        perf_logger.info(f"frontend buy time: {frontend_buy_elapsed}")
        order_elapsed = response["elapsed_time"]
        perf_logger.info(f"order buy time: {order_elapsed}")

        # Use the 'status' boolean in json to check if the purchase was successful
        if response["status"]:
            logging.info("Success")
            return "Successfully purchased: " + str(title) + "\n" + \
                   "Elapsed time (buy, order server): " + str(response["elapsed_time"]) + \
                   "\n" + "Elapsed time (buy, frontend server): " + str(frontend_buy_elapsed)
        else:
            logging.error("Purchase Failed")
            return "Failed to purchase: " + str(title) + "\n" + \
                   "Elapsed time (buy, order server): " + str(response["elapsed_time"]) + \
                   "\n" + "Elapsed time (buy, frontend server): " + str(frontend_buy_elapsed)

    except requests.exceptions.ConnectionError:
        # The chosen replica did not respond. Remove replica from the available order replica list Randomly pick
        # another one to try again
        available_order_list.remove(server_location)
        print("Chosen order replica failed to respond. Trying another replica.")
        return buy(item_number)


if __name__ == "__main__":
    log_path = "logs/frontend.txt"
    open(log_path, "w").close()
    logging.basicConfig(filename=log_path, level=logging.DEBUG, format="%(asctime)s %(message)s")
    FRONTEND_PORT = config['frontend'].split(":")[-1]
    logging.info(f"Frontend server starting on port {FRONTEND_PORT}")

    # Check caching settings
    if len(sys.argv) > 1:
        do_cache_str = sys.argv[1].lower()
        if do_cache_str == "true" or do_cache_str == "t":
            do_caching = True
        elif do_cache_str == "false" or do_cache_str == "f":
            do_caching = False
    else:
        do_caching = True


    # Initially all servers are assumed to be up
    available_order_list = order_replica_list.copy()
    available_catalog_list = catalog_replica_list.copy()

    # Background scheduler that checks for heartbeat every 10 seconds
    scheduler = BackgroundScheduler()
    job = scheduler.add_job(check_heartbeat, 'interval', seconds=3)
    scheduler.start()

    app.run(host='0.0.0.0', port=FRONTEND_PORT)

# Define Flask frontend # Load Server configs from yaml # Server addresses and list of replica addresses # List of possible replica servers # List of available servers # Local dictionary of book titles # Define local cache # Define do_caching, which if false disables caching # Initializers for performance logger # Load balancing between replicas. Randomly choose server replica and return the location of one of the replica of the # requested server type. # Heartbeat check for both the catalog and order replica # Check heartbeat for order replicas # Check heartbeat for catalog replicas # Invalidate a frontend cache. The parameters could be a topic or an item number. # Search for the requested topic # Check cache if the topic is in cache # Parse the json of the search result # Return the search result in a string # The chosen replica did not respond. Randomly pick another one to try again # Lookup the requested item number # Check cache to see if item_number is in cache # Parse the lookup result # Return the lookup result in a string # The chosen replica did not respond. Randomly pick another one to try again # Buy the requested item number # Use the 'status' boolean in json to check if the purchase was successful # The chosen replica did not respond. Remove replica from the available order replica list Randomly pick # another one to try again # Check caching settings # Initially all servers are assumed to be up # Background scheduler that checks for heartbeat every 10 seconds