"""Core API for safecmd"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_core.ipynb.

# %% auto 0
__all__ = ['default_cfg', 'cfg_path', 'ok_ops', 'ok_cmds', 'run', 'CmdSpec', 'parse_cfg', 'validate_cmd', 'DisallowedOps',
           'DisallowedCmd', 'safe_run']

# %% ../nbs/01_core.ipynb 1
import subprocess,json,shutil
from fastcore.utils import *
from fastcore.xdg import xdg_config_home
from configparser import ConfigParser

from .bashxtract import *

# %% ../nbs/01_core.ipynb 11
def run(cmd, ignore_ex=False):
    "Run `cmd` in shell; return stdout (+ stderr if any); raise IOError on failure"
    res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    out = res.stdout.strip()
    if res.stderr: out += ('\n' if out else '') + res.stderr.strip()
    if ignore_ex: return (res.returncode, out)
    if res.returncode: raise IOError(out)
    return out

# %% ../nbs/01_core.ipynb 16
class CmdSpec(BasicRepr):
    def __init__(self,
        name,  # the command (str, will be split into tuple)
        denied=None):  # if set, these flags blocked
        self.name = tuple(name.split())
        self.denied = set(denied or [])

    @classmethod
    def from_str(cls, s):
        "Create from 'cmd:-flag1,-flag2' format"
        name, _, denied = s.partition(':')
        return cls(name, denied.split('|') if denied else None)
        
    def __hash__(self): return hash(self.name)
    def __eq__(self, b): return self.name==b.name
    
    def __repr__(self):
        s = ' '.join(self.name)
        if self.denied: s += f' !{self.denied}'
        return s
    
    def __call__(self, toks):
        "Returns True if allowed, False if no match or denied flag found"
        if tuple(toks[:len(self.name)]) != self.name: return False
        return not (self.denied and self.denied & set(toks))

# %% ../nbs/01_core.ipynb 24
default_cfg = '''[DEFAULT]
ok_ops = |, <, &&, ||, ;

ok_cmds = cat, head, tail, less, more, bat
    # Directory listing
    ls, tree, locate
    # Search
    grep, rg, ag, ack, fgrep, egrep
    # Text processing
    cut, sort, uniq, wc, tr, column
    # File info
    file, stat, du, df, which, whereis, type
    # Comparison
    diff, cmp, comm
    # Archives
    tar, unzip, gunzip, bunzip2, unrar
    # Network
    curl, wget, ping, dig, nslookup, host
    # System info
    date, cal, uptime, whoami, hostname, uname, env, printenv
    # Utilities
    echo, printf, yes, seq, basename, dirname, realpath
    # Git (read-only)
    git log, git show, git diff, git status, git branch, git tag, git remote,
    git stash list, git blame, git shortlog, git describe, git rev-parse,
    git ls-files, git ls-tree, git cat-file, git config --get, git config --list
    # Git (workspace)
    git fetch, git add, git commit, git switch, git checkout
    # Find with deny-list
    find:-exec|-execdir|-delete|-ok|-okdir
    # Builtins
    cd, pwd, export, test, [, true, false
'''

# %% ../nbs/01_core.ipynb 25
cfg_path = xdg_config_home() / 'safecmd' / 'config.ini'
if not cfg_path.exists(): cfg_path.mk_write(default_cfg)

# %% ../nbs/01_core.ipynb 27
def _split_set(s):
    "Split comma-separated string into set of stripped strings"
    return {o.strip() for o in s.split(',')} if s else set()

def _split_specs(s):
    "Split comma-separated string into set of CmdSpecs"
    return {CmdSpec.from_str(c.strip()) for c in s.split(',') if c.strip()} if s else set()

def parse_cfg(cfg_str):
    "Parse config string, return (ok_ops set, ok_cmds set of CmdSpecs)"
    cp = ConfigParser()
    cp.read_string(cfg_str)
    cfg = cp['DEFAULT']
    ok_ops = _split_set(cfg['ok_ops'])
    splitcmds = ','.join(cfg['ok_cmds'].splitlines())
    ok_cmds = _split_specs(splitcmds)
    return ok_ops, ok_cmds

# %% ../nbs/01_core.ipynb 28
ok_ops,ok_cmds = parse_cfg(cfg_path.read_text())

# %% ../nbs/01_core.ipynb 32
def validate_cmd(toks, cmds=None):
    "Check if toks matches an allowed command; returns False if denied flags present"
    if cmds is None: cmds = ok_cmds
    return any(spec(toks) for spec in cmds)

# %% ../nbs/01_core.ipynb 35
class DisallowedOps(PermissionError):
    def __init__(self, ops): super().__init__(f"{ops}")

class DisallowedCmd(PermissionError):
    def __init__(self, cmd): super().__init__(' '.join(cmd))

# %% ../nbs/01_core.ipynb 36
def safe_run(
        cmd:str,  # Bash command string to execute
        cmds:str=None,  # Allowed commands (comma-separated, config format); defaults to ok_cmds
        ops:str=None,  # Allowed operators (comma-separated); defaults to ok_ops
        add_cmds:str=None,  # Temp add these commands
        add_ops:str=None,  # Temp add these operators
        rm_cmds:str=None,  # Temp remove these commands
        rm_ops:str=None,  # Temp remove these operators
    ) -> str:  # Combined stdout/stderr output
    "Run `cmd` in shell if all commands and operators are in allowlists, else raise"
    eff_ops = _split_set(ops) if ops else ok_ops.copy()
    eff_cmds = _split_specs(cmds) if cmds else ok_cmds.copy()
    
    eff_ops |= _split_set(add_ops)
    eff_ops -= _split_set(rm_ops)
    eff_cmds |= _split_specs(add_cmds)
    eff_cmds -= {CmdSpec(c) for c in _split_set(rm_cmds)}
    
    commands, used_ops = extract_commands(cmd)
    if bad_ops := used_ops - eff_ops: raise DisallowedOps(bad_ops)
    for c in commands:
        if not validate_cmd(c, eff_cmds): raise DisallowedCmd(c)
    return run(cmd)
