import threading
import time
import os
import pwd
import grp

import whatap.agent.conf.configure as config
import whatap.util.logging_util as logging_util
from StringIO import StringIO

# File check type constants
FILECHECK_TYPE_UID_CHANGE = 1
FILECHECK_TYPE_GID_CHANGE = 2
FILECHECK_TYPE_PERM_CHANGE = 3
FILECHECK_TYPE_DELETE = 4
FILECHECK_TYPE_MODIFY = 5
FILECHECK_TYPE_NOT_MODIFY = 6

start = False
lock = threading.Lock()


class FileCheckRule:
    """File check rule"""
    def __init__(self, trigger_id=None, trigger_hash=None, severity=0, path=None, rule_types=None):
        self.trigger_id = trigger_id
        self.trigger_hash = trigger_hash
        self.severity = severity
        self.path = path
        self.rule_types = rule_types if rule_types else {}  # {type: option}

    def __str__(self):
        return "FileCheckRule(id={}, hash={}, severity={}, path={}, types={})".format(
            self.trigger_id, self.trigger_hash, self.severity, self.path, self.rule_types)


class FileSnapshot:
    """File state snapshot"""
    def __init__(self, exists=False, uid=-1, gid=-1, perm=0, mod_time=0, size=0, event_triggered=False):
        self.exists = exists
        self.uid = uid
        self.gid = gid
        self.perm = perm
        self.mod_time = mod_time
        self.size = size
        self.event_triggered = event_triggered

    def __str__(self):
        return "FileSnapshot(exists={}, uid={}, gid={}, perm={:03o}, mtime={}, size={})".format(
            self.exists, self.uid, self.gid, self.perm, self.mod_time, self.size)


class FileCheckEvent:
    """File check event"""
    def __init__(self, trigger_id=None, file_path=None, severity=0, check_type=0, before=None, current=None, elapsed=0):
        self.trigger_id = trigger_id
        self.file_path = file_path
        self.severity = severity
        self.check_type = check_type
        self.before = before
        self.current = current
        self.elapsed = elapsed

    def __str__(self):
        return "FileCheckEvent(id={}, path={}, severity={}, type={}, before={}, current={}, elapsed={})".format(
            self.trigger_id, self.file_path, self.severity, self.check_type,
            self.before, self.current, self.elapsed)


# Global state
file_check_rules = {}  # {trigger_hash: FileCheckRule}
file_states = {}  # {trigger_hash: FileSnapshot}
file_check_events = []  # [FileCheckEvent]
fc_lock = threading.Lock()

# UID/GID cache
uid_cache = {}
gid_cache = {}


def uid_name(uid):
    """Convert UID to username"""
    if uid < 0:
        return "-"
    if uid in uid_cache:
        return uid_cache[uid]
    try:
        name = pwd.getpwuid(uid).pw_name
    except KeyError:
        name = str(uid)
    uid_cache[uid] = name
    return name


def gid_name(gid):
    """Convert GID to group name"""
    if gid < 0:
        return "-"
    if gid in gid_cache:
        return gid_cache[gid]
    try:
        name = grp.getgrgid(gid).gr_name
    except KeyError:
        name = str(gid)
    gid_cache[gid] = name
    return name


def stat_file(path):
    """Get file status"""
    try:
        st = os.stat(path)
        return FileSnapshot(
            exists=True,
            uid=st.st_uid,
            gid=st.st_gid,
            perm=st.st_mode & 0o777,
            mod_time=st.st_mtime,
            size=st.st_size
        )
    except OSError:
        return FileSnapshot(exists=False)


def parse_rule_line(key, value):
    """Parse config line

    key format: log.filecheck.TRIGGER_ID.TRIGGER_HASH
    value format: severity,path,rule_types
    rule_types format: type1|type2|type3:option

    example: log.filecheck.123456.abcdef=5,/etc/passwd,1|2|3|6:3600
    """
    key_split = key.split(".")
    if len(key_split) < 4:
        return None

    parts = value.split(",")
    if len(parts) < 3:
        return None

    try:
        severity = int(parts[0].strip())
    except ValueError:
        return None

    path = parts[1].strip()
    spec = parts[2].strip()

    rule = FileCheckRule(
        trigger_id=key_split[-2],
        trigger_hash=key_split[-1],
        severity=severity,
        path=path
    )

    # Parse rule_types
    for tok in spec.split("|"):
        tok = tok.strip()
        if not tok:
            continue
        if ":" in tok:
            try:
                t, opt = tok.split(":", 1)
                rule.rule_types[int(t.strip())] = int(opt.strip())
            except ValueError:
                continue
        else:
            try:
                rule.rule_types[int(tok)] = 0
            except ValueError:
                continue

    return rule


def handle_file_check(rule, prev, now):
    """Check file and generate events"""
    events = []
    cur = stat_file(rule.path)

    if prev is None:
        return cur, events

    path = rule.path
    tid = rule.trigger_id

    # UID change detection
    if FILECHECK_TYPE_UID_CHANGE in rule.rule_types:
        if prev.exists and cur.exists and prev.uid != cur.uid:
            before = uid_name(prev.uid)
            after = uid_name(cur.uid)
            events.append(FileCheckEvent(
                trigger_id=tid,
                file_path=path,
                severity=rule.severity,
                before=before,
                current=after,
                check_type=FILECHECK_TYPE_UID_CHANGE
            ))

    # GID change detection
    if FILECHECK_TYPE_GID_CHANGE in rule.rule_types:
        if prev.exists and cur.exists and prev.gid != cur.gid:
            before = gid_name(prev.gid)
            after = gid_name(cur.gid)
            events.append(FileCheckEvent(
                trigger_id=tid,
                file_path=path,
                severity=rule.severity,
                before=before,
                current=after,
                check_type=FILECHECK_TYPE_GID_CHANGE
            ))

    # Permission change detection
    if FILECHECK_TYPE_PERM_CHANGE in rule.rule_types:
        if prev.exists and cur.exists and prev.perm != cur.perm:
            before = "{:03o}".format(prev.perm)
            after = "{:03o}".format(cur.perm)
            events.append(FileCheckEvent(
                trigger_id=tid,
                file_path=path,
                severity=rule.severity,
                before=before,
                current=after,
                check_type=FILECHECK_TYPE_PERM_CHANGE
            ))

    # Delete detection
    if FILECHECK_TYPE_DELETE in rule.rule_types:
        if prev.exists and not cur.exists:
            events.append(FileCheckEvent(
                trigger_id=tid,
                file_path=path,
                severity=rule.severity,
                check_type=FILECHECK_TYPE_DELETE
            ))

    # Modify detection (mtime or size change)
    if FILECHECK_TYPE_MODIFY in rule.rule_types:
        if prev.exists and cur.exists:
            if prev.mod_time != cur.mod_time or prev.size != cur.size:
                events.append(FileCheckEvent(
                    trigger_id=tid,
                    file_path=path,
                    severity=rule.severity,
                    check_type=FILECHECK_TYPE_MODIFY
                ))

    # Not modified for a certain period detection
    if FILECHECK_TYPE_NOT_MODIFY in rule.rule_types:
        opt = rule.rule_types[FILECHECK_TYPE_NOT_MODIFY]
        if opt > 0 and cur.exists:
            elapsed = now - cur.mod_time
            if elapsed >= opt:
                # Only trigger event if not previously triggered or mtime changed
                if not prev.event_triggered or prev.mod_time != cur.mod_time:
                    events.append(FileCheckEvent(
                        trigger_id=tid,
                        file_path=path,
                        severity=rule.severity,
                        check_type=FILECHECK_TYPE_NOT_MODIFY,
                        elapsed=int(elapsed)
                    ))
                cur.event_triggered = True

    return cur, events


def reload_file_check_config():
    """Reload file check config"""
    global file_check_rules

    while True:
        try:
            fc_lock.acquire()
            file_check_conf = config.GetConfig().searchKey(config.FileCheckWatch)

            new_rules = {}
            for key, value in file_check_conf.items():
                rule = parse_rule_line(key, value)
                if rule:
                    new_rules[rule.trigger_hash] = rule
                    logging_util.debug("FileCheckRule loaded: ", rule)

            file_check_rules.clear()
            file_check_rules.update(new_rules)
        except Exception, e:
            logging_util.debugStack(e)
        finally:
            fc_lock.release()

        time.sleep(10)


def run_file_check_watcher():
    """Run file check watcher"""
    global file_states
    global file_check_events

    while True:
        time.sleep(5)

        try:
            fc_lock.acquire()

            if not file_check_rules:
                continue

            now = time.time()
            events = []
            new_states = {}

            for trigger_hash, rule in file_check_rules.items():
                prev = file_states.get(trigger_hash)
                cur, rule_events = handle_file_check(rule, prev, now)
                new_states[trigger_hash] = cur
                events.extend(rule_events)

            file_states.clear()
            file_states.update(new_states)

            if events:
                file_check_events.extend(events)
                logging_util.debug("FileCheckEvents: ", len(events))
        except Exception, e:
            logging_util.debugStack(e)
        finally:
            fc_lock.release()


def init_file_check_watcher():
    """Initialize file check watcher"""
    global start
    if not start:
        lock.acquire()
        if not start:
            start = True
            t = threading.Thread(target=reload_file_check_config)
            t.daemon = True
            t.start()
            t = threading.Thread(target=run_file_check_watcher)
            t.daemon = True
            t.start()
        lock.release()


def get_file_check_events():
    """Return collected file check events"""
    global file_check_events

    fc_lock.acquire()
    try:
        events = file_check_events[:]
        del file_check_events[:]
        return events
    finally:
        fc_lock.release()


def test():
    init_file_check_watcher()
    while True:
        events = get_file_check_events()
        for e in events:
            print(e)
        time.sleep(1)


if __name__ == '__main__':
    test()
