import crypt
import json
import logging
from contextlib import asynccontextmanager
from pydantic import BaseModel, Field
import os
from subprocess import PIPE, Popen

from fastapi import FastAPI, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

# always update version when doing any changes to this file
VERSION = "1.1"

DEBUG = False
DEBUG_LAST_LINES = 100

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE_PATH = os.path.join(BASE_DIR, "log.txt")
UPDATE_SCRIPT_PATH = os.path.join(BASE_DIR, "fastapi_update.sh")

logger = logging.getLogger("fast_thinlinc")
logging.basicConfig(
    filename=LOG_FILE_PATH,
    level=logging.WARNING,
    format='[%(asctime)s] - %(message)s',
    datefmt='%d/%m/%Y %I:%M:%S %p'
)

apikey = None


def extract_api_key():
    global apikey
    try:
        apikey_file = open("apikey.json", "r")
        apikey_dict = json.loads(apikey_file.read())
        apikey = str(apikey_dict["apikey"])
    except FileNotFoundError:
        logger.error("apikey.json file not found")
        return None
    except json.JSONDecodeError:
        logger.error("apikey.json is not a valid JSON file")
        return None
    except KeyError:
        logger.error('Key "apikey" not found in apikey.json')
        return None
    return apikey


@asynccontextmanager
async def lifespan(app: FastAPI):
    if DEBUG:
        os.environ['PATH'] += ':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/opt/thinlinc/bin:/opt/thinlinc/sbin'
    # startup
    global apikey
    apikey = extract_api_key()
    if not apikey:
        print("API extraction error. Check log file at {}".format(LOG_FILE_PATH))
        exit(1)
    else:
        logger.info("Thinlinc API started successfully")

    yield
    # shutdown
    apikey = None

app = FastAPI(lifespan=lifespan)


@app.middleware("http")
async def security_check_api_key(request: Request, call_next):
    """
        Middleware to check if the request has the correct apiKey.
    """
    # give browser access (no apiKey) to docs
    if request.url.path not in ["/redocs", "/docs", "/openapi.json"]:
        received_apikey = request.headers.get("Authorization")
        if not received_apikey or str(received_apikey) != apikey:
            logger.error("Incorrect apiKey access attempt from {}".format(request.client.host))
            json_error = jsonable_encoder({"error": "Access Denied"})
            return JSONResponse(content=json_error, status_code=status.HTTP_401_UNAUTHORIZED)
    response = await call_next(request)
    return response


class ConfigData(BaseModel):
    admin_email: str = Field(..., min_length=5)
    admin_password: str = Field(..., min_length=1)
    admin_username: str = Field(..., min_length=1)
    domain_name: str = Field(..., min_length=3)


class CertificateData(BaseModel):
    domain_name: str = Field(..., min_length=4)
    admin_email: str = Field(..., min_length=5)


class UserData(BaseModel):
    username: str = Field(..., min_length=4)
    password: str = Field(..., min_length=1)


def execute_safe_command(command: str, approved_exit_status=[], mute=False):
    p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
    stdout, stderr = p.communicate()
    rc = p.returncode

    if not mute:
        if rc and stderr:
            logger.error(command + ' returned via stderr:\n ' + stderr.decode('utf-8'))
        if rc and stdout:
            logger.error(command + ' returned via stdout:\n ' + stdout.decode('utf-8'))
    if not rc or rc in approved_exit_status:
        return True
    else:
        return False


def restart_thinlinc_services():
    """
        Restarts all thinlinc services. Should return None on success
    """
    if not execute_safe_command("systemctl restart tlwebadm.service"):
        return {"message": "restart_service tlwebadm.service"}
    if not execute_safe_command("systemctl restart tlwebaccess.service"):
        return {"message": "restart_service vmserver.service"}
    if not execute_safe_command("systemctl restart vsmagent.service"):
        return {"message": "restart_service vsmagent.service"}
    return None


@app.post("/init_thinlinc_server/", summary="Initialize ThinLinc Service", description="""
Initializes and configures a ThinLinc server with the provided configuration data. The server
          is created from a VHI template.

Request Body:
- `admin_email` (str): The ThinLinc administrator's email address.
- `admin_password` (str): The ThinLinc administrator's password.
- `admin_username` (str): The ThinLinc administrator's username.
- `domain_name` (str): The domain name to be used with the ThinLinc server.

""")
def init_thinlinc_server(config_data: ConfigData, response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    try:
        # configure hostname
        if not execute_safe_command(f"tl-config '/vsmagent/agent_hostname={config_data.domain_name}'"):
            return {"message": "config_thinlinc set_agent_hostname"}

        # configure admin email
        if not execute_safe_command(f"tl-config '/vsmserver/admin_email={config_data.admin_email}'"):
            return {"message": "config_thinlinc set_admin_email"}

        # configure web admin username
        if not execute_safe_command(f"tl-config '/tlwebadm/username={config_data.admin_username}'"):
            return {"message": "config_thinlinc set_admin_username"}

        # configure web admin password
        cp = os.popen(f"tl-gen-auth '{config_data.admin_password}'")
        admin_password_hash = cp.read().strip()
        if cp.close():
            return {"message": "config_thinlinc hash_admin_password"}

        if not execute_safe_command(f"tl-config '/tlwebadm/password={admin_password_hash}'"):
            return {"message": "config_thinlinc set_admin_password"}

    except Exception as e:
        logger.error(str(e))
        return {"message": "error_check_logs"}

    # restart all thinlinc services
    service_output = restart_thinlinc_services()
    if service_output:
        return service_output

    response.status_code = status.HTTP_200_OK
    return {"message": "success"}


@app.post("/generate_certificates/", summary="Generate Certificates", description="""
Generates SSL certificates for the specified domain name using Certbot.
Also symlinks certificates to thinlinc folders and restarts all thinlinc services.

Request Body:
- `domain_name` (str): The domain name to generate the certificates for.
- `admin_email` (str): The domain administrator's email address.
""")
def generate_certificates(config_data: CertificateData, response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    certbot_command = f"certbot certonly --standalone --preferred-challenges http -d '{config_data.domain_name}' --non-interactive --agree-tos -m '{config_data.admin_email}'"
    if DEBUG:
        certbot_command += " --staging"
    if not execute_safe_command(certbot_command):
        return {"message": "generate_certificates"}

    # symlink certificates to thinlinc folders
    if not execute_safe_command(f"ln -sf /etc/letsencrypt/live/{config_data.domain_name}/cert.pem /opt/thinlinc/etc/tlwebaccess/server.crt"):
        return {"message": "generate_certificates symlink"}
    if not execute_safe_command(f"ln -sf /etc/letsencrypt/live/{config_data.domain_name}/privkey.pem /opt/thinlinc/etc/tlwebaccess/server.key"):
        return {"message": "generate_certificates symlink"}
    if not execute_safe_command(f"ln -sf /etc/letsencrypt/live/{config_data.domain_name}/cert.pem /opt/thinlinc/etc/tlwebadm/server.crt"):
        return {"message": "generate_certificates symlink"}
    if not execute_safe_command(f"ln -sf /etc/letsencrypt/live/{config_data.domain_name}/privkey.pem /opt/thinlinc/etc/tlwebadm/server.key"):
        return {"message": "generate_certificates symlink"}

    # also restart all thinlinc services
    service_output = restart_thinlinc_services()
    if service_output:
        return service_output

    response.status_code = status.HTTP_200_OK
    return {"message": "success"}


def check_user_exists(username):
    return execute_safe_command(f"id -u '{username}'", mute=True)


@app.post("/create_user/", summary="Create User", description="""
Creates a new system user with the specified username and password.

Request Body
- `username` (str): The username for the new user.
- `password` (str): The password for the new user.

""")
def create_user(userData: UserData, response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    if check_user_exists(userData.username):
        return {"message": "user_already_exists"}

    salt = crypt.mksalt(crypt.METHOD_SHA512)
    hashed_password = crypt.crypt(userData.password, salt)
    if not execute_safe_command(f"useradd '{userData.username}' -p '{hashed_password}' -m -s '/bin/bash'"):
        return {"message": "create_new_user"}

    response.status_code = status.HTTP_200_OK
    return {"message": "success"}


@app.post("/change_passwd/", summary="Change User Password", description="""
Changes the password for an existing system user.

Request Body:
- `username` (str): The username of the new Linux user.
- `password` (str): The new password for the new Linux user.

""")
def change_password(userData: UserData, response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    if not check_user_exists(userData.username):
        return {"message": "user_not_exists"}
    if not execute_safe_command(f"echo '{userData.username}:{userData.password}' | chpasswd"):
        return {"message": "change_password"}

    response.status_code = status.HTTP_200_OK
    return {"message": "success"}


@app.get("/delete_user/{user_name}/", summary="Delete User", description="""
Forcefully terminates all processes associated with a user and deletes the user from the system.

Path Parameter:
- `user_name` (str): The username of the user to delete.

""")
def delete_user(user_name: str, response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    if not check_user_exists(user_name):
        return {"message": "user_not_exists"}

    # forcefully close all the user's processes
    if not execute_safe_command(f"pkill -9 -u '{user_name}'", approved_exit_status=[1]):
        return {"message": "close_user_process"}
    if not execute_safe_command(f"deluser '{user_name}' --remove-home"):
        return {"message": "delete_user"}

    response.status_code = status.HTTP_200_OK
    return {"message": "success"}


@app.get("/get_users/", summary="Get All Users", description="""
Retrieves a list of all PAM system users, excluding system-level accounts (`nologin` and `uid` > 1000).
""")
def get_users(response: Response):
    response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    try:
        with open("/etc/passwd", "r") as passwd_file:
            users = passwd_file.readlines()
        users_filter_nologin = [u for u in users if "nologin" not in u]
        users_final = [u.split(":")[0] for u in users_filter_nologin if int(u.split(":")[2]) >= 1000]

        response.status_code = status.HTTP_200_OK
        return {"message": "success", "users": users_final}
    except Exception as e:
        logger.error(str(e))
        return {"message": "get_users"}


@app.get("/get_debug_file/", summary="Get Debug File", description="""
Get last DEBUG_LAST_LINES lines of the log file for this API. Also get the version of this API.
""")
def get_debug_file(response: Response):
    try:
        with open(LOG_FILE_PATH, "r") as log_file:
            log_lines = log_file.readlines()[-DEBUG_LAST_LINES:]
        response.status_code = status.HTTP_200_OK
        return {"message": "success", "log": log_lines, "version": VERSION}
    except Exception as e:
        logger.error(str(e))
        response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
        return {"message": "get_debug_file"}


@app.get("/ping/", summary="Ping", description="""
Test endpoint to check whether service is up.
""")
def ping(response: Response):
    response.status_code = status.HTTP_200_OK
    return {"message": "pong"}


@app.get("/update_api/{rollback}/", summary="Update the API remotely", description="""
         Update the API file from dist.i.beebyte.se

         Path Parameter:
         - `rollback` (str): If rollback is set to 'rollback', the API will be rolled back to the previous version,
            maintained in the thinlinc_api.py.bak file.
         """)
def update(rollback: str, response: Response):
    response.status_code = status.HTTP_200_OK
    if rollback == "rollback":
        if not execute_safe_command(f"{UPDATE_SCRIPT_PATH} --rollback"):
            return {"message": "success"}
    else:
        if not execute_safe_command(f"{UPDATE_SCRIPT_PATH}"):
            return {"message": "success"}

