import os
import base64
import json
import time
import threading
import subprocess

try:
    from Queue import Queue, Empty
except ImportError:
    from queue import Queue, Empty

import whatap.agent.conf.configure as config
import whatap.util.logging_util as logging_util
from whatap.util.date_util import DateUtil as dateutil


# Script Manager Error Codes
ERR_SM_REQUEST_GENERAL       = "ER701000"
ERR_SM_UNKNOWN_REQ_METHOD    = "ER701001"
ERR_SM_ACTIVATE_JSON         = "ER701002"
ERR_SM_ACTIVATE_SCRIPT_FILE  = "ER701003"
ERR_SM_ACTIVATE_CRON_CONFIG  = "ER701004"
ERR_SM_DEACTIVATE_NO_JOBLIST = "ER701005"
ERR_SM_DELETE_NO_JOBLIST     = "ER701006"

ERR_SM_EXEC_GENERAL         = "ER702000"
ERR_SM_UNKNOWN_EXEC_METHOD  = "ER702001"
ERR_SM_EXEC_ENQUEUE_FAILED  = "ER702002"
ERR_SM_EXEC_NOT_INITIALIZED = "ER702003"
ERR_SM_EXEC_NO_UUID         = "ER702004"
ERR_SM_SCRIPT_DECODE_BASE64 = "ER702005"
ERR_SM_SCRIPT_TIMEOUT       = "ER702006"
ERR_SM_SCRIPT_EXECUTE       = "ER702007"

ERR_SM_STATUS_GENERAL         = "ER703000"
ERR_SM_STATUS_NOT_INITIALIZED = "ER703001"

ERR_SM_FILE_READ_ERROR = "ER704000"

ERR_SM_FILE_SAVE_GENERAL    = "ER705000"
ERR_SM_FILE_SAVE_ERROR      = "ER705001"
ERR_SM_FILE_SAVE_DONE_ERROR = "ER705002"
ERR_SM_FILE_SAVE_REQUEST    = "ER705004"

ERR_SM_FILE_LIST_GENERAL = "ER706000"
ERR_SM_FILE_LIST_ERROR   = "ER706001"

ERR_SM_FILE_DELETE_GENERAL = "ER707000"
ERR_SM_FILE_DELETE_ERROR   = "ER707001"


# Status constants
STATUS_WAITING  = "waiting"
STATUS_RUNNING  = "running"
STATUS_SUCCESS  = "success"
STATUS_ERROR    = "failed"
STATUS_NOTFOUND = "notfound"

# Method constants
METHOD_EXECUTE        = "execute"
METHOD_EXECUTE_RESULT = "executeResult"
METHOD_ACTIVATE       = "activate"
METHOD_DEACTIVATE     = "deactivate"
METHOD_DELETE         = "delete"

# Column constants
COLUMN_EXECUTE_UUID = "executeUuid"
COLUMN_CONTENT      = "content"
COLUMN_METHOD       = "method"
COLUMN_JOB_LIST     = "jobList"


class ScriptManagerError(Exception):
    def __init__(self, code, message, err=None):
        self.code = code
        self.message = message
        self.err = err
        super(ScriptManagerError, self).__init__(self._format_message())

    def _format_message(self):
        if self.err:
            return "[{}] {}: {}".format(self.code, self.message, self.err)
        return "[{}] {}".format(self.code, self.message)

    def error_code(self):
        return self.code


class ScriptResult(object):
    def __init__(self, stdout="", stderr="", exit_code=0, error=None, start_time=0, end_time=0):
        self.stdout = stdout
        self.stderr = stderr
        self.exit_code = exit_code
        self.error = error
        self.start_time = start_time
        self.end_time = end_time
        self.timestamp = time.time()


class ScriptResultStorage(object):
    def __init__(self, ttl=600):
        self.results = {}
        self.mutex = threading.Lock()
        self.ttl = ttl
        self._start_cleanup()

    def _start_cleanup(self):
        def cleanup_loop():
            while True:
                time.sleep(60)
                self._cleanup_expired()

        t = threading.Thread(target=cleanup_loop)
        t.daemon = True
        t.start()

    def _cleanup_expired(self):
        with self.mutex:
            now = time.time()
            expired = [uuid for uuid, result in self.results.items()
                       if now - result.timestamp > self.ttl]
            for uuid in expired:
                del self.results[uuid]

    def store(self, execute_uuid, stdout, stderr, exit_code, error, start_time, end_time):
        with self.mutex:
            self.results[execute_uuid] = ScriptResult(
                stdout=stdout,
                stderr=stderr,
                exit_code=exit_code,
                error=error,
                start_time=start_time,
                end_time=end_time
            )

    def get(self, execute_uuid):
        with self.mutex:
            return self.results.get(execute_uuid), execute_uuid in self.results

    def get_and_delete(self, execute_uuid):
        with self.mutex:
            result = self.results.get(execute_uuid)
            exists = execute_uuid in self.results
            if exists:
                del self.results[execute_uuid]
            return result, exists


class ScriptRunner(object):
    def __init__(self, capacity=10, timeout=5, result_ttl=600):
        self.queue = Queue(maxsize=capacity)
        self.script_timeout = timeout
        self.storage = ScriptResultStorage(ttl=result_ttl)
        self.queued_jobs = {}
        self.queue_mutex = threading.Lock()
        self.running_uuid = ""
        self.running_mutex = threading.Lock()
        self.running_start_time = 0
        self.max_queue_size = capacity
        self._running = False

    def start(self):
        self._running = True
        logging_util.info("[ScriptRunner] Started")

        def run_loop():
            while self._running:
                try:
                    try:
                        command_map = self.queue.get(timeout=0.1)
                    except Empty:
                        continue

                    execute_uuid = command_map.get(COLUMN_EXECUTE_UUID, "")
                    if not execute_uuid:
                        logging_util.error("[ScriptRunner] Missing executeUuid in command")
                        continue

                    with self.queue_mutex:
                        if execute_uuid in self.queued_jobs:
                            del self.queued_jobs[execute_uuid]

                    start_time = dateutil.now()
                    with self.running_mutex:
                        self.running_uuid = execute_uuid
                        self.running_start_time = start_time

                    command_base64 = command_map.get(COLUMN_CONTENT, "")
                    end_time = dateutil.now()

                    try:
                        command = base64.b64decode(command_base64)
                        if isinstance(command, bytes):
                            command = command.decode('utf-8', 'replace')
                    except Exception as e:
                        logging_util.error("[ScriptRunner] Base64 decode error: {}".format(e))
                        decode_err = ScriptManagerError(ERR_SM_SCRIPT_DECODE_BASE64,
                                                        "failed to decode base64 script content", e)
                        self.storage.store(execute_uuid, "", "", -1, decode_err, start_time, end_time)
                        with self.running_mutex:
                            self.running_uuid = ""
                        continue

                    stdout, stderr, exit_code, error = self._run_command(command, self.script_timeout)
                    end_time = dateutil.now()

                    max_output_length = 8000
                    if len(stdout) > max_output_length:
                        stdout = stdout[:max_output_length] + "..."
                    if len(stderr) > max_output_length:
                        stderr = stderr[:max_output_length] + "..."

                    if error:
                        logging_util.error("[ScriptRunner] Execute error: {}".format(error))
                        execute_err = ScriptManagerError(ERR_SM_SCRIPT_EXECUTE,
                                                         "failed to execute script", error)
                        self.storage.store(execute_uuid, stdout, stderr, exit_code, execute_err, start_time, end_time)
                    else:
                        logging_util.debug("[ScriptRunner] Execute success")
                        self.storage.store(execute_uuid, stdout, stderr, exit_code, None, start_time, end_time)

                    with self.running_mutex:
                        self.running_uuid = ""

                except Exception as e:
                    logging_util.error("[ScriptRunner] Loop error: {}".format(e))

        t = threading.Thread(target=run_loop)
        t.daemon = True
        t.start()

    def _run_command(self, command, timeout):
        stdout = ""
        stderr = ""
        exit_code = 0
        error = None
        output_data = [None, None]
        proc_error = [None]

        try:
            proc = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=True
            )

            def communicate_thread():
                try:
                    output_data[0], output_data[1] = proc.communicate()
                except Exception as e:
                    proc_error[0] = e

            comm_thread = threading.Thread(target=communicate_thread)
            comm_thread.start()
            comm_thread.join(timeout)

            if comm_thread.is_alive():
                proc.kill()
                proc.wait()
                error = Exception("Timeout after {} seconds".format(timeout))
                exit_code = -1
            elif proc_error[0]:
                error = proc_error[0]
                exit_code = -1
            else:
                exit_code = proc.returncode
                out = output_data[0]
                err = output_data[1]
                if out:
                    if isinstance(out, bytes):
                        stdout = out.decode('utf-8', 'replace')
                    else:
                        stdout = out
                if err:
                    if isinstance(err, bytes):
                        stderr = err.decode('utf-8', 'replace')
                    else:
                        stderr = err

        except Exception as e:
            error = e
            exit_code = -1

        return stdout, stderr, exit_code, error

    def enqueue(self, command_map):
        execute_uuid = command_map.get(COLUMN_EXECUTE_UUID, "")
        if not execute_uuid:
            raise ScriptManagerError(ERR_SM_EXEC_NO_UUID, "missing executeUuid in command", None)

        try:
            self.queue.put_nowait(command_map)
            with self.queue_mutex:
                self.queued_jobs[execute_uuid] = True
        except:
            raise ScriptManagerError(ERR_SM_EXEC_ENQUEUE_FAILED, "script runner queue full", None)

    def is_queued(self, execute_uuid):
        with self.queue_mutex:
            return execute_uuid in self.queued_jobs

    def is_running(self, execute_uuid):
        with self.running_mutex:
            return self.running_uuid == execute_uuid

    def get_queue_status(self):
        with self.queue_mutex:
            return {
                'current': len(self.queued_jobs),
                'total': self.max_queue_size
            }

    def get_running_start_time(self, execute_uuid):
        with self.running_mutex:
            if self.running_uuid == execute_uuid:
                return self.running_start_time
            return 0


# Global script runner instance
_script_runner = None


def init_script_manager():
    global _script_runner
    conf = config.GetConfig()
    queue_size = getattr(conf, 'ScriptManagerExecQueueSize', 10)
    timeout = getattr(conf, 'CronjobTimeout', 5)
    result_ttl = 600  # 10 minutes

    _script_runner = ScriptRunner(capacity=queue_size, timeout=timeout, result_ttl=result_ttl)
    _script_runner.start()
    logging_util.info("[ScriptManager] Initialized")


def execute_script(command_map):
    global _script_runner
    if _script_runner is None:
        raise ScriptManagerError(ERR_SM_EXEC_NOT_INITIALIZED, "script runner not initialized", None)

    method = command_map.get(COLUMN_METHOD, "")

    if method == METHOD_EXECUTE:
        _script_runner.enqueue(command_map)
        return None

    elif method == METHOD_EXECUTE_RESULT:
        execute_uuid = command_map.get(COLUMN_EXECUTE_UUID, "")
        if not execute_uuid:
            raise ScriptManagerError(ERR_SM_EXEC_NO_UUID, "missing executeUuid for executeResult", None)
        return get_script_status(execute_uuid)

    else:
        raise ScriptManagerError(ERR_SM_UNKNOWN_EXEC_METHOD,
                                 "invalid method for execution: {}".format(method), None)


def get_script_status(execute_uuid):
    global _script_runner
    if _script_runner is None:
        raise ScriptManagerError(ERR_SM_EXEC_NOT_INITIALIZED, "script runner not initialized", None)

    queue_status = _script_runner.get_queue_status()

    # 1. Check if running
    if _script_runner.is_running(execute_uuid):
        start_time = _script_runner.get_running_start_time(execute_uuid)
        return {
            'uuid': execute_uuid,
            'status': STATUS_RUNNING,
            'queue_status': queue_status,
            'start_time': start_time
        }

    # 2. Check if queued
    if _script_runner.is_queued(execute_uuid):
        return {
            'uuid': execute_uuid,
            'status': STATUS_WAITING,
            'queue_status': queue_status
        }

    # 3. Check completed results
    result, exists = _script_runner.storage.get_and_delete(execute_uuid)
    if exists:
        status = STATUS_SUCCESS if result.exit_code == 0 else STATUS_ERROR
        response = {
            'uuid': execute_uuid,
            'status': status,
            'queue_status': queue_status,
            'start_time': result.start_time,
            'end_time': result.end_time,
            'stdout': result.stdout,
            'stderr': result.stderr,
            'exit_code': result.exit_code
        }
        if result.error:
            response['error'] = result.error
        return response

    # 4. Not found
    return {
        'uuid': execute_uuid,
        'status': STATUS_NOTFOUND,
        'queue_status': queue_status
    }


def get_running_script_status():
    global _script_runner
    if _script_runner is None:
        raise ScriptManagerError(ERR_SM_EXEC_NOT_INITIALIZED, "script runner not initialized", None)

    queue_status = _script_runner.get_queue_status()

    with _script_runner.running_mutex:
        running_uuid = _script_runner.running_uuid
        running_start_time = _script_runner.running_start_time

    if running_uuid:
        return {
            'uuid': running_uuid,
            'status': STATUS_RUNNING,
            'start_time': running_start_time,
            'queue_status': queue_status
        }

    return {
        'uuid': '',
        'status': STATUS_NOTFOUND,
        'queue_status': queue_status
    }


def read_file(file_path, pos, length, direction="forward"):
    try:
        if not os.path.exists(file_path):
            raise Exception("File not found: {}".format(file_path))

        file_size = os.path.getsize(file_path)

        with open(file_path, 'r') as f:
            if direction == "backward":
                start_pos = max(0, pos - length)
                f.seek(start_pos)
                text = f.read(min(length, pos))
                end_pos = pos
            else:
                f.seek(pos)
                text = f.read(length)
                start_pos = pos
                end_pos = pos + len(text)

        return text, start_pos, end_pos, file_size, None

    except Exception as e:
        return "", 0, 0, 0, e


def list_files():
    conf = config.GetConfig()
    cron_dir = getattr(conf, 'CronjobPath', '/usr/whatap/infra/cronjob')

    try:
        if not os.path.exists(cron_dir):
            return [], None

        files = []
        for f in os.listdir(cron_dir):
            full_path = os.path.join(cron_dir, f)
            if os.path.isfile(full_path):
                files.append(f)
        return files, None

    except Exception as e:
        return [], e


def delete_file(file_name):
    conf = config.GetConfig()
    cron_dir = getattr(conf, 'CronjobPath', '/usr/whatap/infra/cronjob')
    file_path = os.path.join(cron_dir, os.path.basename(file_name))

    try:
        if os.path.exists(file_path):
            os.remove(file_path)
        return None
    except Exception as e:
        return e


# File save state for chunked uploads
_file_save_state = {}
_file_save_mutex = threading.Lock()


def save_file_chunk(file_name, pos, data):
    conf = config.GetConfig()
    cron_dir = getattr(conf, 'CronjobPath', '/usr/whatap/infra/cronjob')

    if not os.path.exists(cron_dir):
        try:
            os.makedirs(cron_dir)
        except Exception as e:
            raise ScriptManagerError(ERR_SM_FILE_SAVE_ERROR,
                                     "failed to create directory: {}".format(cron_dir), e)

    file_path = os.path.join(cron_dir, os.path.basename(file_name))
    tmp_path = file_path + ".tmp"

    try:
        mode = 'ab' if pos > 0 else 'wb'
        with open(tmp_path, mode) as f:
            if pos > 0:
                f.seek(pos)
            f.write(data)

        with _file_save_mutex:
            _file_save_state[file_name] = tmp_path

        return None

    except Exception as e:
        raise ScriptManagerError(ERR_SM_FILE_SAVE_ERROR,
                                 "failed to save file chunk", e)


def save_file_done(file_name, size, crc):
    conf = config.GetConfig()
    cron_dir = getattr(conf, 'CronjobPath', '/usr/whatap/infra/cronjob')
    file_path = os.path.join(cron_dir, os.path.basename(file_name))
    tmp_path = file_path + ".tmp"

    try:
        if os.path.exists(tmp_path):
            # Verify size
            actual_size = os.path.getsize(tmp_path)
            if actual_size != size:
                os.remove(tmp_path)
                raise ScriptManagerError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                         "size mismatch: expected {}, got {}".format(size, actual_size), None)

            # Make executable on unix
            os.chmod(tmp_path, 0o755)

            # Atomic rename
            if os.path.exists(file_path):
                os.remove(file_path)
            os.rename(tmp_path, file_path)

            with _file_save_mutex:
                if file_name in _file_save_state:
                    del _file_save_state[file_name]

            return None
        else:
            raise ScriptManagerError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                     "temp file not found: {}".format(tmp_path), None)

    except ScriptManagerError:
        raise
    except Exception as e:
        raise ScriptManagerError(ERR_SM_FILE_SAVE_DONE_ERROR,
                                 "failed to finalize file save", e)
