from silence.sql.tables import get_tables, get_views, get_primary_key, is_auto_increment, get_primary_key_views
from silence.logging.default_logger import logger
from silence.settings import settings

from os import listdir, getcwd, path, mkdir, makedirs
from shutil import rmtree
import json
import re

# Entry point for the CLI command
def create_api():
    existing_routes_method_pairs = get_user_endpoints()
    logger.info("Found the following user defined endpoints:")
    for rmp in existing_routes_method_pairs:
        logger.info(rmp)

    create_entity_endpoints(existing_routes_method_pairs)

#############################################################################
# Get the entities from the database and the existing user endpoints and    #
# create CRUD endpoint files (json) for the remaining ones.                 #
#############################################################################
def create_entity_endpoints(existing_routes_method_pairs):
    '''
    Reset the "auto" folder for endpoints and iterate over the databases tables and views,
    generating the corresponding endpoints for each of them.
    '''
    # Remove the folder if it exists and create it again
    auto_dir = getcwd() + "/endpoints/auto"
    
    try:
        rmtree(auto_dir)
    except FileNotFoundError:
        logger.debug("Endpoint folder not found.")
    
    logger.debug("Creating directory %s", auto_dir)
    makedirs(auto_dir)

    # Create the endpoints (JSON and JS) for the tables and views
    # We combine the tables and views in one list of tuples,
    # adding a "is_table" flag to distinguish them.
    tables_data = [(td, True) for td in get_tables().items()]
    views_data = [(vd, False) for vd in get_views().items()]
    all_data = tables_data + views_data

    for (table_name, columns), is_table in all_data:
        # We always write all autogenerated endpoints to the JS files,
        # while only new endpoints that have not been defined by the
        # user are added as JSON config files for the server
        endpoints_to_js = {}
        endpoints_to_json = {}
        logger.info("Generating endpoints and JS API files for %s", table_name)

        if is_table:
            pk = get_primary_key(table_name)
            route_one = f"/{table_name.lower()}/${pk}"

            if pk and is_auto_increment(table_name, pk):
                columns.remove(pk)

        # Create the basic CRUD endpoints if they haven't been defined yet
        route_all = f"/{table_name.lower()}"
        crud_endpoints = [
            ("GET", "getAll", route_all),
            ("GET", "getById", route_one),
            ("POST", "create", route_all),
            ("PUT", "update", route_one),
            ("DELETE", "delete", route_one),
        ]

        for method, name, route in crud_endpoints:
            # If this is a view or a table without a PK, create only "getAll"
            if name != "getAll" and (not is_table or pk is None): 
                continue

            endpoints_to_js[name] = make_endpoint_data(name, route, method, table_name, columns, pk)
            if (route, method) not in existing_routes_method_pairs:
                endpoints_to_json[name] = endpoints_to_js[name]

        # If this is a view, we generate additional JS functions based on the
        # primary keys from other tables it contains, for easier querying
        if not is_table:
            for pk in get_primary_key_views(table_name):
                endpoint_name = "getBy" + pk[0].upper() + pk[1:]  # Is there a better way to do this?
                endpoints_to_js[endpoint_name] = {
                    "route": f"{route_all}?{pk}=${pk}",
                    "method": "GET",
                    "description": f"Gets an array entries from '{table_name}' by their {pk}.\n{' '*8}Note that this always returns an array."
                }

        # Create *all* the .js files for the API.
        generate_API_file_for_endpoints(endpoints_to_js, table_name, pk)

        # Save the *new* endpoints as JSON config to a file
        if endpoints_to_json:
            json_str = json.dumps(endpoints_to_json, indent=4)
            with open(auto_dir + f"/{table_name}.json", "w", encoding="utf8") as f:
                f.write(json_str)

    # Finally, generate the .js API module for the allowed auth operations
    auth_endpoints = {}
    if settings.ENABLE_LOGIN:
        auth_endpoints["login"] = { "route": "/login", "method": "POST", "description": "Logs in using an identifier and password" }

    if settings.ENABLE_REGISTER:
        auth_endpoints["register"] = { "route": "/register", "method": "POST", "description": "Registers a new user and stores the password safely in the database" }

    if auth_endpoints:
        generate_API_file_for_endpoints(auth_endpoints, "auth", None)

###############################################################################
# Creates a dict with the auto-generated data for a basic CRUD endpoint
##############################################################################
def make_endpoint_data(name, route, method, table_name, cols, pk):
    sql, desc = {
        "getAll": (
            f"SELECT * FROM {table_name}",
            f"Gets all entries from '{table_name}'"
        ),
        "getById": (
            f"SELECT * FROM {table_name} WHERE {pk} = ${pk}",
            f"Gets an entry from '{table_name}' by its primary key",
        ),
        "create": (
            f"INSERT INTO {table_name} " + params_to_string(cols) + " VALUES " + params_to_string(cols, "$"),
            f"Creates a new entry in '{table_name}'"
        ),
        "update": (
            f"UPDATE {table_name} SET " + params_to_string(cols, is_update=True) + f" WHERE {pk} = ${pk}",
            f"Updates an existing entry in '{table_name}' by its primary key"
        ),
        "delete": (
            f"DELETE FROM {table_name} WHERE {pk} = ${pk}",
            f"Deletes an existing entry in '{table_name}' by its primary key"
        ),
    }[name]

    data = {
        "description": desc,
        "route": route,
        "method": method,
        "sql": sql,
        "request_body_params": cols,
        "auth_required": method != "GET",
        "allowed_roles": ["*"],
    }

    if method not in ("POST", "PUT"):
        del data["request_body_params"]
    
    return data

###############################################################################
# Create generic .js files to consume the created endpoints.
###############################################################################  
def generate_API_file_for_endpoints(endpoints, table_name, pk_name):
    api_path = getcwd() + "/web/js/api"
    
    if not path.isdir(api_path):
        makedirs(api_path)

    functions = [generate_api_text(ep_name, pk_name, ep_data) for ep_name, ep_data in endpoints.items()]
    functions_str = "\n\n    ".join(functions)

    file_content = f"""/*
 * DO NOT EDIT THIS FILE, it is auto-generated. It will be updated automatically.
 * All changes done to this file will be lost upon re-running the 'silence createapi' command.
 * If you want to create new API methods, define them in a new file.
 *
 * Silence is built and maintained by the DEAL research group at the University of Seville.
 * You can find us at https://deal.us.es
 */

"use strict";

import {{ BASE_URL, requestOptions }} from './common.js';

const {table_name}API_auto = {{

    {functions_str}
}};

export {{ {table_name}API_auto }};"""

    open(f"{api_path}/_{table_name}.js", "w", encoding="utf8").write(file_content)


def generate_api_text(name, pk_name, endpoint_data):
    method = endpoint_data["method"].lower()
    route = endpoint_data["route"]
    description = endpoint_data["description"]

    # Replace the URL params with JS's string interpolation method
    if "$" in route:
        route = re.sub(r"\$(\w+)", r"${\1}", route)

    is_getById = name == "getById"
    needs_formdata = method in ("post", "put")
    formdata = ", formData" if needs_formdata else ""

    args = []
    if needs_formdata: 
        args.append("formData")

    if method in ("put", "delete") or is_getById: 
        args.append(pk_name)
    
    return f"""/** {description} */
    {name}: async function({', '.join(args)}) {{
        let response = await axios.{method}(`${{BASE_URL}}{route}`{formdata}, requestOptions);
        return response.data{"[0]" if is_getById else ""};
    }},"""

def get_user_endpoints():
    '''
    Searches in the endpoints directory of the silence project for every .json file which we assume is an endpoint file.
    gets the route and method as pairs for every empoint found and returns them
    '''
    logger.debug("Looking for user endpoints")
    endpoints_dir = getcwd() + "/endpoints"

    # Create the endpoints dir if its not there
    if not path.isdir(endpoints_dir):
        mkdir(endpoints_dir)

    # Get every .json file in the directory
    user_endpoints = [endpoints_dir + f"/{f}" for f in listdir(endpoints_dir) if f.endswith(".json")]
    endpoint_route_method_pairs = []

    # Form (route, method) pairs for every endpoint found and return them
    for jsonfile in user_endpoints:
        with open(jsonfile, "r", encoding="utf8") as ep:
            endpoints = list(json.load(ep).values())
            endpoint_pair = [(endpoint['route'], endpoint['method']) for endpoint in endpoints]
            endpoint_route_method_pairs += endpoint_pair

    return endpoint_route_method_pairs

def params_to_string(param_list, char_add="", is_update=False):
    add_pref = lambda x: char_add + x if not is_update else f"{x} = ${x}"
    res = ", ".join(add_pref(x) for x in param_list)
    return f"({res})" if not is_update else res