import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import tempfile
import time
import uuid

from transport import OperationsError


class DatabaseDrill:
    def __init__(self, root, image="mysql:8.4"):
        if not re.fullmatch(r"mysql:8\.4(?:\.\d+)?", image):
            raise OperationsError("The recovery drill requires an official MySQL 8.4 image")
        self.root = Path(root)
        self.image = image
        self.name = "post76-restore-" + uuid.uuid4().hex
        self.environment = {"PATH": os.environ["PATH"], "DOCKER_HOST": "unix:///var/run/docker.sock",
                            "DOCKER_CONFIG": str(self.root / "docker-config")}

    def command(self, arguments, input_file=None, allowed_failure=False):
        result = subprocess.run(["docker", *arguments], env=self.environment, stdin=input_file,
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if result.returncode and not allowed_failure:
            raise OperationsError("Isolated database drill failed (exit " + str(result.returncode) + "); output is withheld")
        return result

    def run(self, sql_file, database):
        if not isinstance(database, str) or not re.fullmatch(r"[A-Za-z0-9_]{1,64}", database):
            raise OperationsError("The recovery database requires a safe identifier")
        memory = self.command(["info", "--format", "{{.MemTotal}}"]).stdout
        if int(memory.strip()) < 6 * 1024**3:
            raise OperationsError("Run database drills on a recovery host with at least 6 GiB Docker memory; the 4 GB live host is insufficient")
        with tempfile.TemporaryDirectory(prefix="drill-", dir=self.root) as temporary:
            password = Path(temporary) / "root_password"
            password.write_text(secrets.token_hex(32))
            password.chmod(0o444)
            created = False
            try:
                created = True
                self.command(["run", "--detach", "--name", self.name, "--network", "none", "--memory", "896m",
                              "--security-opt", "no-new-privileges:true", "--mount",
                              "type=bind,source=" + str(password) + ",target=/run/secrets/root_password,readonly",
                              "--env", "MYSQL_ROOT_PASSWORD_FILE=/run/secrets/root_password", "--env", "MYSQL_DATABASE=" + database,
                              self.image, "--innodb-buffer-pool-size=256M", "--performance-schema=OFF"])
                for attempt in range(90):
                    result = self.command(["exec", self.name, "sh", "-c",
                                           'MYSQL_PWD="$(cat /run/secrets/root_password)" exec mysql --protocol=socket --user=root --batch --skip-column-names "$1" -e "SELECT 1"',
                                           "recovery-ready", database],
                                          allowed_failure=True)
                    if result.returncode == 0 and result.stdout.strip() == b"1":
                        break
                    time.sleep(2)
                else:
                    raise OperationsError("Isolated recovery database did not become ready")
                with Path(sql_file).open("rb") as source:
                    self.command(["exec", "-i", self.name, "sh", "-c",
                                  'MYSQL_PWD="$(cat /run/secrets/root_password)" exec mysql --protocol=socket --user=root "$1"',
                                  "recovery-import", database], source)
                result = self.command(["exec", self.name, "sh", "-c",
                                       'MYSQL_PWD="$(cat /run/secrets/root_password)" exec mysql --protocol=socket --user=root --batch --skip-column-names "$1" -e "SHOW TABLES"',
                                       "recovery-check", database])
                tables = result.stdout.decode().splitlines()
                if not tables:
                    raise OperationsError("Recovery import produced no tables")
                return {"database_imported": True, "tables": len(tables), "isolated_network": True, "production_database_modified": False}
            finally:
                if created:
                    self.command(["rm", "--force", "--volumes", self.name])
