#!/usr/bin/env python3
"""Inspect the SylkServer Cassandra message journal for an account.

Progressive drill-down:

  sylk-dump-message-cassandra --accounts                                 list all accounts (+ message counts)
  sylk-dump-message-cassandra <account>                                  list contacts we have messages with (+ counts)
  sylk-dump-message-cassandra <account> <contact>                        counts per message content-type
  sylk-dump-message-cassandra <account> <contact> --type T               counts per day (YYYYMMDD) for that type
  sylk-dump-message-cassandra <account> <contact> --type T --date D      dump the actual messages for that day
  sylk-dump-message-cassandra <account> --id ID                          dump a specific message by its message_id

--type matches a content-type exactly or by prefix (e.g. 'text' matches text/plain
and text/html). --date is YYYYMMDD. This is a standalone companion to sylk-db; it
only reads from the Cassandra backend.
"""
import argparse
import logging
import os
import sys
from collections import Counter

# Allow running from the tools/ subdirectory: make the parent SylkServer
# directory (which holds the `sylk` package) importable.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from application import log
from application.process import process

# Keep the output clean (like the SQLite tool): silence the Cassandra driver's
# INFO chatter (datacenter / host-discovery messages).
logging.getLogger('cassandra').setLevel(logging.WARNING)

CASSANDRA_MODULES_AVAILABLE = False
try:
    from cassandra.cqlengine import connection
except ImportError:
    pass
else:
    try:
        from cassandra.cqlengine.models import Model
    except ImportError:
        pass
    else:
        CASSANDRA_MODULES_AVAILABLE = True
        from cassandra.cluster import (EXEC_PROFILE_DEFAULT, Cluster,
                                       ExecutionProfile, NoHostAvailable)
        from cassandra.policies import DCAwareRoundRobinPolicy


class colors:
    if sys.stdout.isatty():
        TGREEN = '\033[32m'
        ENDC = '\033[m'
        BOLD = '\033[1m'
    else:
        TGREEN = ''
        ENDC = ''
        BOLD = ''


class parse_level:
    def format(record):
        if record.levelname != 'INFO':
            return f'{record.levelname:<8s} '
        else:
            return f"{'  ......':<8s} "


def bold(string):
    return colors.BOLD + string + colors.ENDC


def _cassandra_connect(tables=()):
    """Open a blocking Cassandra session for the CLI and register the keyspace on
    the given cqlengine table models (see sylk-db for the rationale)."""
    from sylk.applications.webrtcgateway.configuration import CassandraConfig
    profile = ExecutionProfile(load_balancing_policy=DCAwareRoundRobinPolicy(), request_timeout=60)
    cluster = Cluster(CassandraConfig.cluster_contact_points, protocol_version=4,
                      execution_profiles={EXEC_PROFILE_DEFAULT: profile})
    try:
        session = cluster.connect()
    except NoHostAvailable:
        log.warning("Can't connect to Cassandra cluster")
        sys.exit()
    connection.set_session(session)
    for table in tables:
        table.__keyspace__ = CassandraConfig.keyspace
    return cluster


def _msg_datetime(message):
    """Best timestamp for a message: the original msg_timestamp if present,
    otherwise the journal created_at."""
    return message.msg_timestamp or message.created_at


def _msg_day(message):
    dt = _msg_datetime(message)
    return dt.strftime('%Y%m%d') if dt else '????????'


def _type_matches(content_type, wanted):
    if not wanted:
        return True
    if not content_type:
        return False
    return content_type == wanted or content_type.startswith(wanted)


def _chat_message_model():
    from sylk.applications.webrtcgateway.configuration import CassandraConfig
    from sylk.applications.webrtcgateway.models.storage.cassandra import ChatMessage
    log.Formatter.prefix_format = parse_level
    log.Formatter.prefix_length = 0
    if not (CASSANDRA_MODULES_AVAILABLE and CassandraConfig.cluster_contact_points):
        log.warning('Cassandra is not available — this tool only supports the Cassandra backend')
        sys.exit()
    _cassandra_connect([ChatMessage])
    return ChatMessage


def _account_messages(account):
    ChatMessage = _chat_message_model()
    return ChatMessage.objects(ChatMessage.account == account).limit(None)


def list_accounts():
    print(f"\n{' SylkServer - Message accounts ':*^80s}\n")
    ChatMessage = _chat_message_model()
    counts = Counter()
    for account in ChatMessage.objects().limit(None).values_list('account', flat=True):
        counts[account] += 1
    total = sum(counts.values())
    print(f'{bold(str(total))} message(s) stored across {bold(str(len(counts)))} account(s)\n')
    for account, n in counts.most_common():
        print(f'{n:>8}  {account}')
    if not counts:
        print('No messages found')


def list_contacts(account):
    print(f"\n{' SylkServer - Message contacts ':*^80s}\n")
    counts = Counter()
    for m in _account_messages(account):
        counts[m.contact] += 1
    total = sum(counts.values())
    print(f'{bold(str(total))} message(s) stored for {bold(account)} across '
          f'{bold(str(len(counts)))} contact(s)\n')
    for peer, n in counts.most_common():
        print(f'{n:>8}  {peer}')
    if not counts:
        print('No messages found')


def counts_per_type(account, contact=None, mtype=None):
    print(f"\n{' SylkServer - Message types ':*^80s}\n")
    counts = Counter()
    for m in _account_messages(account):
        if contact and m.contact != contact:
            continue
        if not _type_matches(m.content_type, mtype):
            continue
        counts[m.content_type] += 1
    total = sum(counts.values())
    who = f'with {bold(contact)}' if contact else f'for {bold(account)}'
    print(f'{bold(str(total))} message(s) {who}\n')
    for ct, n in counts.most_common():
        print(f'{n:>8}  {ct}')
    if not counts:
        print('No messages found')


def counts_per_day(account, contact=None, mtype=None, month=None):
    print(f"\n{' SylkServer - Messages per day ':*^80s}\n")
    counts = Counter()
    for m in _account_messages(account):
        if contact and m.contact != contact:
            continue
        if not _type_matches(m.content_type, mtype):
            continue
        d = _msg_day(m)
        if month and not d.startswith(month):
            continue
        counts[d] += 1
    total = sum(counts.values())
    label = f'{bold(mtype)} message(s)' if mtype else 'message(s)'
    who = f'with {bold(contact)}' if contact else f'for {bold(account)}'
    scope = f' in {bold(month)}' if month else ''
    print(f'{bold(str(total))} {label} {who}{scope} across {bold(str(len(counts)))} day(s)\n')
    for day in sorted(counts):
        print(f'{day}  {counts[day]:>8}')
    if not counts:
        print('No messages found')


def counts_per_month(account, contact=None, mtype=None):
    print(f"\n{' SylkServer - Messages per month ':*^80s}\n")
    counts = Counter()
    for m in _account_messages(account):
        if contact and m.contact != contact:
            continue
        if not _type_matches(m.content_type, mtype):
            continue
        counts[_msg_day(m)[:6]] += 1
    total = sum(counts.values())
    label = f'{bold(mtype)} message(s)' if mtype else 'message(s)'
    who = f'with {bold(contact)}' if contact else f'for {bold(account)}'
    print(f'{bold(str(total))} {label} {who} across {bold(str(len(counts)))} month(s)\n')
    for month in sorted(counts):
        print(f'{month}  {counts[month]:>8}')
    if not counts:
        print('No messages found')


def dump_messages(account, contact=None, mtype=None, day=None, month=None, message_id=None):
    print(f"\n{' SylkServer - Message dump ':*^80s}\n")
    rows = []
    for m in _account_messages(account):
        if message_id is not None and m.message_id != message_id:
            continue
        if contact and m.contact != contact:
            continue
        if not _type_matches(m.content_type, mtype):
            continue
        d = _msg_day(m)
        if day is not None and d != day:
            continue
        if month is not None and not d.startswith(month):
            continue
        rows.append(m)
    rows.sort(key=lambda m: (_msg_datetime(m) or m.created_at))
    label = f'{bold(mtype)} message(s)' if mtype else 'message(s)'
    who = f'with {bold(contact)}' if contact else f'for {bold(account)}'
    if message_id is not None:
        period = f' with message_id {bold(message_id)}'
    else:
        period = f' on {bold(day)}' if day else (f' in {bold(month)}' if month else '')
    print(f'{bold(str(len(rows)))} {label} {who}{period}\n')
    for m in rows:
        dt = _msg_datetime(m)
        ts = dt.isoformat() if dt else '?'
        content = m.content
        if isinstance(content, (bytes, bytearray)):
            content = content.decode('utf-8', 'ignore')
        print(f"{'-' * 80}")
        print(f'{"timestamp:":>14} {ts}')
        print(f'{"direction:":>14} {m.direction}')
        print(f'{"contact:":>14} {m.contact}')
        print(f'{"message_id:":>14} {m.message_id}')
        print(f'{"content_type:":>14} {m.content_type}')
        print(f'{"state:":>14} {m.state}')
        print(f'{"content:":>14} {content}')
    if not rows:
        print('No messages found')


if __name__ == '__main__':
    process.configuration.subdirectory = 'sylkserver'

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('-h', '--help',
                        action='help',
                        default=argparse.SUPPRESS,
                        help='Show this help message and exit.')

    parser.add_argument("--config-dir",
                        dest='config_directory',
                        default=None,
                        metavar='PATH',
                        help="Set a config directory.")

    parser.add_argument('account', nargs='?', help='Account')
    parser.add_argument('contact', nargs='?', help='contact to inspect')
    parser.add_argument('--accounts', action='store_true',
                        help='list all accounts (with message counts, sorted desc)')
    parser.add_argument('--type', dest='mtype', metavar='CONTENT_TYPE',
                        help='message content-type (exact or prefix, e.g. text)')
    parser.add_argument('--types', action='store_true',
                        help='list the message content-types (with counts)')
    parser.add_argument('--days', action='store_true',
                        help='list the days that have messages (with counts)')
    parser.add_argument('--months', action='store_true',
                        help='list the months that have messages (with counts)')
    parser.add_argument('--date', metavar='YYYYMMDD', help='day in YYYYMMDD format')
    parser.add_argument('--month', metavar='YYYYMM', help='month in YYYYMM format')
    parser.add_argument('--id', dest='message_id', metavar='MESSAGE_ID',
                        help='dump a specific message by its message_id')

    options = parser.parse_args()

    if options.config_directory is not None:
        process.configuration.local_directory = options.config_directory

    log.Formatter.prefix_format = parse_level
    log.Formatter.prefix_length = 0

    from sylk.server import ServerConfig

    log.Formatter.prefix_format = parse_level
    log.Formatter.prefix_length = 0

    ServerConfig.__cfgtype__(ServerConfig.__cfgfile__)

    cfg_dir = os.path.abspath(options.config_directory) if options.config_directory else os.getcwd()
    print(f'Configuration directory: {bold(cfg_dir)}')
    from application.configuration import ConfigFile
    cfg_files = ConfigFile('config.ini').files + ConfigFile('webrtcgateway.ini').files
    if cfg_files:
        print(f'Reading configuration from: {", ".join(cfg_files)}')

    # The contact (positional), --type, --date and --month act as filters and
    # combine with whichever view is selected. The view flags are mutually
    # exclusive; none of them requires a contact.
    if options.accounts:
        list_accounts()
        sys.exit()

    if options.account is None:
        parser.error('account is required unless --accounts is given')

    if options.message_id is not None:
        dump_messages(options.account, options.contact, options.mtype, message_id=options.message_id)
    elif options.types:
        counts_per_type(options.account, options.contact, options.mtype)
    elif options.days:
        counts_per_day(options.account, options.contact, options.mtype, month=options.month)
    elif options.months:
        counts_per_month(options.account, options.contact, options.mtype)
    elif options.date is not None:
        dump_messages(options.account, options.contact, options.mtype, day=options.date)
    elif options.month is not None:
        dump_messages(options.account, options.contact, options.mtype, month=options.month)
    elif options.mtype is not None:
        counts_per_day(options.account, options.contact, options.mtype)
    elif options.contact is not None:
        counts_per_type(options.account, options.contact)
    else:
        list_contacts(options.account)
