
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from collections import defaultdict
import argparse
import json
import sqlite3
import sys

import six

from . import expose

from vdsm import client
from vdsm.config import config
from vdsm import utils

_BLANK_UUID = '00000000-0000-0000-0000-000000000000'
_NAME = 'dump-volume-chains'


class DumpChainsError(Exception):
    pass


class NoConnectedStoragePoolError(DumpChainsError):
    pass


class ChainError(DumpChainsError):
    def __init__(self, volumes_children):
        self.volumes_children = volumes_children


class DuplicateParentError(ChainError):
    description = ("more than one volume pointing to the same parent volume "
                   "e.g: (_BLANK_UUID<-a), (a<-b), (a<-c)")


class NoBaseVolume(ChainError):
    description = ("no volume with a parent volume Id _BLANK_UUID found e.g: "
                   "(a<-b), (b<-c)")


class ChainLoopError(ChainError):
    description = ("a loop found in the volume chain. This happens if a "
                   "volume points to one of it's parent volume e.g.: "
                   "(BLANK_UUID<-a), (a<-b), (b<-c), (c<-a)")


class OrphanVolumes(ChainError):
    description = ("there are volumes that are part of an image and are "
                   "pointing to volumes which are not part of the chain e.g: "
                   "(BLANK_UUID<-a), (a<-b), (c<-d)")


@expose(_NAME)
def dump_chains(*args):
    parsed_args = _parse_args(args)
    cli = client.connect(parsed_args.host, parsed_args.port,
                         use_tls=parsed_args.use_ssl)
    with utils.closing(cli):
        volumes_info = _get_volumes_info(cli, parsed_args.sd_uuid)
        if parsed_args.output == 'text':
            image_chains = _get_volumes_chains(volumes_info)
            _print_volume_chains(image_chains, volumes_info)
        elif parsed_args.output == 'json':
            json.dump(volumes_info, sys.stdout, indent=2)
        elif parsed_args.output == 'sqlite':
            _dump_sql(volumes_info, parsed_args.sqlite_file)
        else:
            raise ValueError('unknown output format %s' % parsed_args.output)


def _parse_args(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('sd_uuid', help="storage domain UUID")
    parser.add_argument('-u', '--unsecured', action='store_false',
                        dest='use_ssl', default=True,
                        help="use unsecured connection")
    parser.add_argument('-H', '--host', default='localhost')
    parser.add_argument('-o', '--output', choices=['text', 'json', 'sqlite'],
                        default='text', help="select output format")
    parser.add_argument(
        '-p', '--port', default=config.getint('addresses', 'management_port'))
    parser.add_argument('-f', '--sqlite-file', help="sqlite3 db output file")

    parsed_args = parser.parse_args(args=args[1:])

    if parsed_args.output == 'sqlite' and parsed_args.sqlite_file is None:
        parser.error("--output sqlite requires --sqlite-file.")

    return parsed_args


def _dump_sql(volumes_info, sql_file):
    with sqlite3.connect(sql_file) as con:
        con.executescript("""
            DROP TABLE IF EXISTS volumes;
            CREATE TABLE volumes(
                uuid UUID,
                parent UUID,
                image UUID,
                status TEXT,
                voltype TEXT,
                format TEXT,
                legality TEXT,
                type TEXT,
                disktype TEXT,
                capacity UNSIGNED INTEGER,
                apparentsize UNSIGNED INTEGER,
                truesize UNSIGNED INTEGER,
                ctime UNSIGNED INTEGER
            );
            """)
        con.executemany("""
            INSERT INTO volumes VALUES (
                :uuid,
                :parent,
                :image,
                :status,
                :voltype,
                :format,
                :legality,
                :type,
                :disktype,
                :capacity,
                :apparentsize,
                :truesize,
                :ctime
            );
            """, _iter_volumes_info(volumes_info))


def _iter_volumes_info(volumes_info):
    for _, img_volumes in six.iteritems(volumes_info):
        for _, vol_info in six.iteritems(img_volumes):
            yield vol_info


def _get_volumes_info(cli, sd_uuid):
    pools = cli.Host.getConnectedStoragePools()
    if not pools:
        raise NoConnectedStoragePoolError('There is no connected storage '
                                          'pool to this server')
    sp_uuid, = pools
    images_uuids = cli.StorageDomain.getImages(storagedomainID=sd_uuid)

    volumes_info = {}

    for img_uuid in images_uuids:
        img_volumes_info = {}

        volumes_uuids = cli.StorageDomain.getVolumes(
            storagedomainID=sd_uuid, storagepoolID=sp_uuid,
            imageID=img_uuid)

        for vol_uuid in volumes_uuids:
            vol_info = cli.Volume.getInfo(
                volumeID=vol_uuid, storagepoolID=sp_uuid,
                storagedomainID=sd_uuid, imageID=img_uuid)

            img_volumes_info[vol_uuid] = vol_info

        volumes_info[img_uuid] = img_volumes_info

    return volumes_info


def _get_volumes_chains(volumes_info):
    image_chains = {}

    for img_uuid, volumes in six.iteritems(volumes_info):

        for vol_uuid, vol_info in six.iteritems(volumes):
            volumes_children.append((vol_info['parent'], vol_uuid))

        try:
            image_chains[img_uuid] = _build_volume_chain(volumes_children)
        except ChainError as e:
            image_chains[img_uuid] = e

    return image_chains


def _build_volume_chain(volumes_children):
    volumes_by_parents = dict(volumes_children)
    if len(volumes_by_parents) < len(volumes_children):
        raise DuplicateParentError(volumes_children)

    child_vol = _BLANK_UUID
    while True:
        child_vol = volumes_by_parents.get(child_vol)
        if child_vol is None:
        if child_vol in chain:
            raise ChainLoopError(volumes_children)
        chain.append(child_vol)

    if not chain and volumes_by_parents:
        raise NoBaseVolume(volumes_children)

    if len(chain) < len(volumes_by_parents):
        raise OrphanVolumes(volumes_children)

    return chain


def _print_volume_chains(image_chains, volumes_info):
    if not image_chains:
        print()
        _print_line("(no images found)")
        print()
        return
    print()
    print('Images volume chains (base volume first)')
    for img_uuid, vol_chain in six.iteritems(image_chains):
        img_volumes_info = volumes_info[img_uuid]
        print()
        _print_line(img_uuid, 'image:')
        print()
        if isinstance(vol_chain, ChainError):
            chain_err = vol_chain
            _print_error(chain_err.description)
            print()
            _print_line('Unordered volumes and children:')
            print()
            for parent, child in chain_err.volumes_children:
                _print_line('- %s <- %s' % (parent, child))
                _print_vol_info(img_volumes_info[child])
                print()
        else:
            for vol in vol_chain:
                _print_line('- ' + vol)
                _print_vol_info(img_volumes_info[vol])
                print()


def _print_vol_info(volume_info):
    robust_volume_info = defaultdict(lambda: '(missing)', volume_info)
    info_fmt = "status: {d[status]}, voltype: {d[voltype]}, " \
               "format: {d[format]}, legality: {d[legality]}, " \
               "type: {d[type]}, capacity: {d[capacity]}, " \
               "truesize: {d[truesize]}"
    formatted_info = info_fmt.format(d=robust_volume_info)
    _print_line('  ' + formatted_info)


def _print_line(body, title=''):
    print('{0:^13}{1}'.format(title, body))


def _print_error(body, title=''):
    print('{0:^13}Error: {1}'.format(title, body))
