from typing import Optional, List, Dict
import uuid
import json
from datetime import datetime


class BPFtraceError(Exception):
    """BPFtrace general error"""


class MetricType:
    """BPFtrace variable types"""
    Control = 'control'
    Histogram = 'histogram'
    Stacks = 'stacks'
    Output = 'output'


class VariableDefinition:
    def __init__(self, single: bool, semantics: int, datatype: int, metrictype: MetricType):
        self.single = single
        self.semantics = semantics
        self.datatype = datatype
        self.metrictype = metrictype


class ScriptMetadata:
    def __init__(self):
        self.name: Optional[str] = None
        self.include: Optional[List[str]] = None
        self.table_retain_lines: Optional[int] = None
        self.custom_output_block = False


class Status:
    Stopped = 'stopped'
    Starting = 'starting'  # starting can take a while
    Started = 'started'
    Stopping = 'stopping'  # if the process doesn't respond to SIGINT, wait 5s for SIGKILL
    Error = 'error'  # stopped and error occured (bpftrace error or process manager error)


class State:
    def __init__(self):
        self.status = Status.Stopped
        self.reset()

    def reset(self):
        self.pid = -1
        self.exit_code = 0
        self.error = ''
        self.probes = 0
        self.data_bytes = 0
        self.data = {}


class Script:
    def __init__(self, code: str):
        # PMNS metric names must start with an alphabetic character
        self.script_id = 's' + str(uuid.uuid4()).replace('-', '')
        self.username: Optional[str] = None
        self.persistent = False
        self.created_at = datetime.now()
        self.last_accessed_at = datetime.now()
        self.code = code
        self.metadata = ScriptMetadata()
        self.variables: Dict[str, VariableDefinition] = {}
        self.state = State()

    def __str__(self) -> str:
        data = []
        if self.metadata.name:
            data.append(f"name={self.metadata.name}")
        if self.username:
            data.append(f"user={self.username}")
        else:
            data.append("user=anonymous")
        if self.state.pid != -1:
            data.append(f"PID={self.state.pid}")

        if data:
            return f"script {self.script_id} ({', '.join(data)})"
        else:
            return f"script {self.script_id}"


class ScriptEncoder(json.JSONEncoder):
    def __init__(self, *args, dump_state_data=True, **kwargs):
        super().__init__(*args, **kwargs)
        self.dump_state_data = dump_state_data

    # pylint: disable=arguments-differ,method-hidden
    def default(self, obj):
        if isinstance(obj, State) and not self.dump_state_data:
            state = obj.__dict__.copy()
            del state["data"]
            return state
        elif isinstance(obj, (Script, ScriptMetadata, VariableDefinition, State)):
            return obj.__dict__
        elif isinstance(obj, datetime):
            return obj.isoformat()
        else:
            return json.JSONEncoder.default(self, obj)


class PMDAConfig:
    # see bpftrace.conf for configuration descriptions and units
    class DynamicScriptsConfig:
        def __init__(self):
            self.enabled = False
            self.auth_enabled = True
            self.allowed_users = []

    def __init__(self):
        self.dynamic_scripts = PMDAConfig.DynamicScriptsConfig()

        self.bpftrace_path = 'bpftrace'
        self.script_expiry_time = 60  # 1 min
        self.max_throughput = 2 * 1024 * 1024  # 2 MB/s


class RuntimeInfo:
    def __init__(self):
        # assuming latest version per default (simplifies version checks)
        self.bpftrace_version = (999, 999, 999)
        self.bpftrace_version_str = ""


class Logger:
    def __init__(self, info, error):
        self.info = info
        self.error = error
