"""
Description.
"""
__author__ = 'Dan Gunter <dkgunter@lbl.gov>'
__date__ = '2/21/13'


from email.mime.text import MIMEText
import json
from operator import itemgetter
import smtplib
#
from .util import DoesLogging, JsonWalker
from ..util import MongoJSONEncoder
from .diff import Differ, Delta  # for field constants, formatting


class Report:
    def __init__(self, header):
        """Create blank report, with a header.

        :param header: Report header
        :type header: Header
        """
        self._hdr = header
        self._sections = []

    def add_section(self, section):
        self._sections.append(section)

    @property
    def header(self):
        return self._hdr

    def is_empty(self):
        if len(self._sections) == 0:
            return True
        self._total_rows = 0
        for sect in self._sections:
            self._count_rows(sect)
        return self._total_rows == 0

    def _count_rows(self, sect):
        if sect.body is not None:
            self._total_rows += sect.body.nrow
        for subsect in sect._sections:
            self._count_rows(subsect)

    def __iter__(self):
        return iter(self._sections)


class ReportSection(Report):
    """Section within a report, with data.
    """
    def __init__(self, header, body=None):
        """Create new report section, initialized with header and body.

        :param header: The header for the section
        :type header: SectionHeader
        :param body: The body of the section, or None if this is a container for sub-sections
        :type body: Table
        """
        Report.__init__(self, header)
        self._body = body

    @property
    def body(self):
        return self._body


class Header:
    """Base header class.
    """
    def __init__(self, title=''):
        self._kv = []
        self.title = title

    def add(self, key, value):
        self._kv.append((key, value))

    def get(self, key):
        return (v for k, v in self._kv if k == key)

    def __iter__(self):
        return iter(self._kv)

    def as_dict(self):
        return {k: v for k, v in self._kv}


class ReportHeader(Header):
    """Header for entire report.
    """
    pass


class SectionHeader(Header):
    """Header for one section of a report.
    """
    pass


class Table:
    """Table of values.
    """
    def __init__(self, colnames):
        self._colnames = colnames
        self._rows = []
        self._width = len(colnames)
        self._max_col_widths = list(map(len, colnames))

    def add(self, values):
        if len(values) != self._width:
            raise ValueError('expected {:d} values, got {:d}'.format(self._width, len(values)))
        self._rows.append(values)
        for i, v in enumerate(values):
            n = len(str(v))
            if self._max_col_widths[i] < n:
                self._max_col_widths[i] = n

    def sortby(self, name_or_index):
        name, index = None, None
        if isinstance(name_or_index, int):
            index = name_or_index
        else:
            name = name_or_index
        if name is not None:
            try:
                colnum = self._colnames.index(name)
            except ValueError:
                raise ValueError('column {} not in {}'.format(name, self._colnames))
        else:
            if index < 0 or index >= self._width:
                raise ValueError('index out of range 0..{:d}'.format(self._width - 1))
            colnum = index
        self._rows.sort(key=itemgetter(colnum))

    def __iter__(self):
        return iter(self._rows)

    @property
    def values(self):
        return [{self._colnames[i]: r[i] for i in range(self._width)}
                for r in self._rows]

    @property
    def column_names(self):
        return self._colnames

    @property
    def column_widths(self):
        return self._max_col_widths

    @property
    def ncol(self):
        return self._width

    @property
    def nrow(self):
        return len(self._rows)


## Exceptions

class ReportBackupError(Exception):
    pass

## Formatting


def css_minify(s):
    #return s.replace('\n', ' ').replace('  ', ' ')
    s = s.replace("{ ", "{")
    s = s.replace(" }", "}")
    return s

# CSS for HTML report output
DEFAULT_CSS = [
    "html { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }",
    "body { margin: 2em;}",
    "table { margin-top: 1em; clear: both; border: 0;}",
    "dl, dt, dd { float: left; }",
    "dl, dt { clear: both; }",
    "dt { width: 8em; font-weight: 700; }",
    "dd { width: 32em; }",
    "tr:nth-child(even) { background-color: #E9E9E9; }",
    "tr:nth-child(odd) { background-color: #E9E9E9; }",
    "th, td {padding: 0.2em 0.5em;}",
    "th { text-align: left;  color: black; margin: 0; font-weight: 300;}",
    "h1, h2, h3 { clear: both; margin: 0; padding: 0; }",
    "h1 { font-size: 18; color: rgb(44, 62, 80); }",
    "h2 { font-size: 14; color: black; }"
]


class HTMLFormatter:
    """Format a report as HTML.
    """
    def __init__(self, line_sep='\n', id_column=0, css=None):
        self._sep = line_sep
        self._idcol = id_column
        if css is None:
            css = DEFAULT_CSS
        self._css = css

    def format(self, report):
        text = []
        text.append('<!DOCTYPE html>')
        text.append('<html>')
        text.append('<title>{}</title>'.format(report.header.title))
        text.append('<head>')
        if self._css:
            text.append('<style>')
            text.append("\r\n".join(self._css))
            text.append('</style>')
        text.append('</head>')
        text.append('<body>')
        text.append('<h1>{}</h1>'.format(report.header.title))
        text.append('<dl class="rptmeta">')
        for key, value in report.header:
            text.append('<dt>{}</dt>'.format(key))
            text.append('<dd>{}</dd>'.format(value))
        text.append('</dl>')
        for section in report:
            text.append('<h2>{}</h2>'.format(section.header.title))
            text.append('<dl class="sectmeta">')
            for key, value in section.header:
                text.append('<dt>{}</dt>'.format(key))
                text.append('<dd>{}</dd>'.format(value))
            text.append('</dl>')
            for cond_section in section:
                text.append('<h3>{}</h3>'.format(cond_section.header.title))
                text.append('<dl class="subsectmeta">')
                for key, value in cond_section.header:
                    text.append('<dt>{}</dt>'.format(key))
                    text.append('<dd>{}</dd>'.format(value))
                text.append('</dl>')
                text.append('<table>')
                text.append('<tr>')
                for name in cond_section.body.column_names:
                    text.append('<th>{}</th>'.format(name))
                text.append('</tr>')
                prev_key, i = None, 0
                for row in cond_section.body:
                    row = list(row)
                    key = row[self._idcol]
                    if prev_key and key == prev_key:
                        row[self._idcol] = ''
                    else:
                        prev_key = key
                        i += 1
                    rclass = ('even', 'odd')[i % 2]
                    text.append('<tr class="{}">'.format(rclass))
                    for value in row:
                        text.append('<td>{}</td>'.format(value))
                    text.append('</tr>')
                text.append('</table>')
        text.append('</body>')
        text.append('</html>')
        text_str = self._sep.join(text)
        text_str = text_str.replace("**", "<br>")
        return text_str

class JSONFormatter:
    """Format a report as JSON.
    """
    def __init__(self, id_column=0, indent=2):
        self._indent = indent
        self._idcol = id_column

    def format(self, report):
        obj = dict(
            title=report.header.title,
            info=report.header,
            sections=[
                dict(title=s.header.title,
                     info=s.header,
                     conditions=[dict(
                         title=cs.header.title,
                         info=cs.header,
                         violations=cs.body)
                         for cs in s
                     ]
                )
                for s in report
            ]
        )
        return json.dumps(obj, indent=self._indent, cls=MongoJSONEncoder)


class ReportJSONEncoder(MongoJSONEncoder):
    def default(self, o):
        if isinstance(o, Header):
            return o.as_dict()
        elif isinstance(o, Table):
            return o.values
        return MongoJSONEncoder.default(self, o)


class MarkdownFormatter:
    """Format a report as markdown
    """
    def __init__(self, id_column=0):
        self._idcol = id_column

    def _mapdump(self, d):
        return ', '.join((('{}={}'.format(k, v) for k, v in d.items())))

    def _fixed_width(self, values, widths):
        s = ''.join(["{{:{:d}s}}".format(w + 1).format(str(v))
                    for w, v in zip(widths, values)])
        return s

    def format(self, report):
        lines = []
        self._append_heading(lines, 1, report.header.title)
        self._append_info_section(lines, report.header)
        for section in report:
            self._append_heading(lines, 2, section.header.title)
            self._append_info_section(lines, section.header)
            for cond in section:
                self._append_heading(lines, 3, cond.header.title)
                self._append_info_section(lines, cond.header)
                self._append_violations(lines, cond.body)
        return '\n'.join(lines)

    def _append_info_section(self, lines, info):
        if not info:
            return
        infodict = info.as_dict()
        if infodict:
            text = 'Info: {}'.format(self._mapdump(infodict))
            lines.append(text)

    def _append_heading(self, lines, level, title):
        hashes = '#' * level
        text = '\n{} {} {}\n'.format(hashes, title, hashes)
        lines.append(text)

    def _append_violations(self, lines, data):
        lines.append('\nViolations:\n')
        indent = '    '
        lines.append(indent + self._fixed_width(data.column_names, data.column_widths))
        for row in data:
            lines.append(indent + self._fixed_width(row, data.column_widths))


class Emailer(DoesLogging):
    """Send a report to an email recipient.
    """
    def __init__(self, sender='me@localhost', recipients=('you@remote.host',),
                 subject='Report', server='localhost', port=None, **kwargs):
        """Send reports as email.

        :param: sender Sender of the email
        :param: recipients List of _recipients of the email
        :param: subject Email _subject line
        :param: server SMTP server host
        :param: port SMTP server port (None for default)
        """
        DoesLogging.__init__(self, 'mg.emailer')
        self._sender, self._recipients, self._subject = (sender, recipients,
                                                         subject)
        self._server, self._port = server, port
        self._message = ""

    @property
    def subject(self):
        return self._subject

    @subject.setter
    def subject(self, value):
        self._subject = value

    def send(self, text, fmt):
        """Send the email message.

        :param text: The text to send
        :type text: str
        :param fmt: The name of the format of the text
        :type fmt: str
        :return: Number of recipients it was sent to
        :rtype: int
        """
        main_fmt, sub_fmt = fmt.split('/')
        if sub_fmt.lower() == "text":
            msg = MIMEText(text, "plain")
        elif sub_fmt.lower() == "html":
            msg = MIMEText(text, "html")
        else:
            raise ValueError("Unknown message format: {}".format(fmt))
        msg['Subject'] = self._subject
        msg['From'] = self._sender
        msg['To'] = ', '.join(self._recipients)
        if self._port is None:
            conn_kwargs = dict(host=self._server)
        else:
            conn_kwargs = dict(host=self._server, port=self._port)
        self._log.info("connect to email server {}".format(conn_kwargs))
        try:
            s = smtplib.SMTP(**conn_kwargs)
            #s.set_debuglevel(2)
            refused = s.sendmail(self._sender, self._recipients, msg.as_string())
            if refused:
                self._log.warn("Email to {:d} recipients was refused".format(len(refused)))
                for person, (code, msg) in refused.items():
                    self._log("Email to {p} was refused ({c}): {m}".format(p=person, c=code, m=msg))
            s.quit()
            n_recip = len(self._recipients)
        except Exception as err:
            self._log.error("connection to SMTP server failed: {}".format(err))
            n_recip = 0
        return n_recip


# ---------------
# Diff formatting
# ---------------

class DiffFormatter:
    """Base class for formatting a 'diff' report.
    """

    TITLE = "Materials Project Database Diff Report"

    def __init__(self, meta, key=None):
        """Constructor.

        :param meta: Report metadata, must have the following keys:
                     - start_time, end_time: string repr of report gen. times
                     - elapsed: float #sec for end_time - start_time
                     - db1, db2: string repr of 2 input database/collections.
        :type meta: dict
        :param key: Record key field
        :type key: str
        """
        self.meta = meta
        self.key = key

    def format(self, result):
        """Format a report from a result object.

        :return: Report body
        :rtype: str
        """
        raise NotImplementedError()

    def result_subsets(self, rs):
        """Break a result set into subsets with the same keys.

        :param rs: Result set, rows of a result as a list of dicts
        :type rs: list of dict
        :return: A set with distinct keys (tuples), and a dict, by these tuples, of max. widths for each column
        """
        keyset, maxwid = set(), {}
        for r in rs:
            key = tuple(sorted(r.keys()))
            keyset.add(key)
            if key not in maxwid:
                maxwid[key] = [len(k) for k in key]
            for i, k in enumerate(key):
                strlen = len("{}".format(r[k]))
                maxwid[key][i] = max(maxwid[key][i], strlen)
        return keyset, maxwid

    def ordered_cols(self, columns, section):
        """Return ordered list of columns, from given columns and the name of the section
        """
        columns = list(columns)  # might be a tuple
        fixed_cols = [self.key]
        if section.lower() == "different":
            fixed_cols.extend([Differ.CHANGED_MATCH_KEY, Differ.CHANGED_OLD, Differ.CHANGED_NEW])
        map(columns.remove, fixed_cols)
        columns.sort()
        return fixed_cols + columns

    def sort_rows(self, rows, section):
        """Sort the rows, as appropriate for the section.

        :param rows: List of tuples (all same length, same values in each position)
        :param section: Name of section, should match const in Differ class
        :return: None; rows are sorted in-place
        """
        #print("@@ SORT ROWS:\n{}".format(rows))
        # Section-specific determination of sort key
        if section.lower() == Differ.CHANGED.lower():
            sort_key = Differ.CHANGED_DELTA
        else:
            sort_key = None
        if sort_key is not None:
            rows.sort(key=itemgetter(sort_key))


class DiffJsonFormatter(DiffFormatter):

    class Encoder(json.JSONEncoder):
        def default(self, o):
            if hasattr(o, 'as_json'):
                return o.as_json()
            return json.JSONEncoder.default(self, o)

#    class Manipulator(pymongo.son_manipulator.SONManipulator):
#        def transform_incoming(self, son, collection):
#            return walk(son, visit_as_json, None)

    def __init__(self, meta, pretty=False, **kwargs):
        """Constructor.

        :param meta: see superclass
        :param pretty: Indented format
        :type pretty: bool
        """
        DiffFormatter.__init__(self, meta, **kwargs)
        self._indent = 4 if pretty else None

    def will_copy(self):
            return True

    def _add_meta(self, result):
        # put metadata into its own section
        result['meta'] = self.meta
        # .. but promote a timestamp, for searching
        result['time'] = self.meta['end_time']

    def format(self, result):
        self._add_meta(result)
        return json.dumps(result, cls=self.Encoder, indent=self._indent)

    def document(self, result):
        """Build dict for MongoDB, expanding result keys as we go.
        """
        self._add_meta(result)
        walker = JsonWalker(JsonWalker.value_json, JsonWalker.dict_expand)
        r = walker.walk(result)
        return r


class DiffHtmlFormatter(DiffFormatter):
    """Format an HTML diff report.
    """
    DIFF_CSS = [
        ".header {padding: 5px; margin: 0 5px;}",
        ".header h1 {color: #165F4B; font-size: 20; text-align: left; margin-left: 20px;}",
        ".header p {color: #666666; margin-left: 20px; height: 12px;}",
        ".header p em {color: #4169E1; font-style: normal;}",
        ".content {padding: 15px; padding-top: 0px; margin: 0; background-color: #F3F3F3;}",
        ".content h2 {color: #2C3E50; font-size: 16px;}",
        ".empty { font-size: 14px; font-style: italic;}",
        ".section {padding: 5px; margin: 10px; background-color: #E2E2E2; border-radius: 5px;}",
        ".section div {margin-left: 10px;}",
        ".section table {margin-left: 5px;}",
        "tr:nth-child(even) { background-color: white; }",
        "tr:nth-child(odd) { background-color: #F5F5F5; }",
        "tr:nth-child(1) { background-color: #778899; font-weight: 500;}",
        "th, td {padding: 0.2em 0.5em;}",
        "th { text-align: left;  color: white; margin: 0;}",
        ".fixed { font-family: Consolas, monaco, monospace; }",
    ]
    css = DEFAULT_CSS + DIFF_CSS

    # for email inlining
    styles = {
        "header": {
            "h1": "color: #165F4B; font-size: 20; text-align: left; margin-left: 20px",
            "p":  "color: #666666; margin-left: 20px; height: 12px",
            "em": "color: #4169E1; font-style: normal"
        },
        "content": {
            "_": "padding: 15px; padding-top: 0px; margin: 0; background-color: #F3F3F3",
            "h2": "color: #2C3E50; font-size: 16px",
            "section": "padding: 5px; margin: 10px; background-color: #E2E2E2; border-radius: 5px"
        },
        "table": {
            "table": "margin-top: 1em; clear: both; border: 0",
            "tr_even": "background-color: white",
            "tr_odd": "background-color: #F5F5F5",
            "tr1": "background-color: #778899; font-weight: 500",
            "th": "text-align: left;  color: white; margin: 0; padding: 0.2em 0.5em",
            "td": "padding: 0.2em 0.5em"
        }
    }

    def __init__(self, meta, url=None, email_mode=False, **kwargs):
        """Constructor.

        :param meta: see superclass
        :param url: Optional URL to create hyperlink for keys
        :type url: str
        """
        DiffFormatter.__init__(self, meta, **kwargs)
        self._url = url
        self._email = email_mode

    def format(self, result):
        """Generate HTML report.

        :return: Report body
        :rtype: str
        """
        css = "\n".join(self.css)
        content = "{}{}".format(self._header(), self._body(result))
        if self._email:
            text = """<!DOCTYPE html>
            <html>
            <div width="100%" style="{sty}">{content}</div>
            </html>
            """.format(css=css, content=content, sty=self.styles["content"]["_"])
        else:
            text = """<html>
            <head><style>{css}</style></head>
            <body>{content}</body>
            </html>
            """.format(css=css, content=content)
        return text

    def _header(self):
        lines = ["<div class='header'><h1{{sh1}}>{t}</h1>".format(t=self.TITLE),
                 "<p{sp}>Compared <em{sem}>{{db1}}</em> with <em{sem}>{{db2}}</em></p>",
                 "<p{sp}>Filter: <span class='fixed'>{{filter}}</span></p>",
                 "<p{sp}>Run time: <em{sem}>{{start_time}}</em> to <em{sem}>{{end_time}}</em> ",
                 "(<em{sem}>{{elapsed:.1f}}</em> sec)</p>",
                 "</div>"]
        if self._email:  # inline the styles
            _c = "header"
            _f = lambda s: s.format(sp=self.style(_c, "p"), sem=self.style(_c, "em"), sh1=self.style(_c, "h1"))
        else:
            _f = lambda s: s.format(sp="", sem="", sh1="")
        lines = map(_f, lines)
        s = "\n".join(lines)
        return s.format(**self.meta)

    def style(self, css_class, elt):
        s = ""
        if css_class in self.styles and elt in self.styles[css_class]:
            s = " style='{}'".format(self.styles[css_class][elt])
        return s

    def _body(self, result):
        body = ["<div class='content'>"]
        for section in result.keys():
            body.append("<div class='section'{{ssec}}><h2{{sh2}}>{t}</h2>".format(t=section.title()))
            if len(result[section]) == 0:
                body.append("<div class='empty'>Empty</div>")
            else:
                body.extend(self._table(section, result[section]))
            body.append("</div>")
        body.append("</div>")
        if self._email:
            _c = "content"
            _f = lambda s: s.format(s_=self.style(_c, "_"), sh2=self.style(_c, "h2"), ssec=self.style(_c, "section"))
        else:
            _f = lambda s: s.format(s_="", sh2="", ssec="")
        body = map(_f, body)
        return '\n'.join(body)

    def _table(self, section, rows):
        if self._email:
            inline = {k: self.style("table", k) for k in self.styles["table"]}
        else:
            inline = dict.fromkeys(self.styles["table"], "")
        subsets, _ = self.result_subsets(rows)
        tables = []
        for subset in subsets:
            tables.append("<table{table}>".format(**inline))
            cols = self.ordered_cols(subset, section)
            # Format the table.
            tables.extend(["<tr{tr1}>".format(**inline)] +
                          ["<th{th}>{c}</th>".format(c=c, **inline) for c in cols] +
                          ["</tr>"])
            self.sort_rows(rows, section)
            for i, r in enumerate(rows):
                tr = "{{tr_{}}}".format(("even", "odd")[i % 2])
                if tuple(sorted(r.keys())) != subset:
                    continue
                if self._url is not None:
                    r[cols[0]] = "<a href='{p}{v}'>{v}</a>".format(p=self._url, v=r[cols[0]])
                tables.extend(["<tr{}>".format(tr).format(**inline)] +
                              ["<td{td}>{d}</td>".format(d=r[c], **inline) for c in cols] +
                              ["</tr>"])
            tables.append("</table>")
        return tables


class DiffTextFormatter(DiffFormatter):
    """Format a plain-text diff report.
    """

    def format(self, result):
        """Generate plain text report.

        :return: Report body
        :rtype: str
        """
        m = self.meta
        lines = ['-' * len(self.TITLE),
                 self.TITLE,
                 '-' * len(self.TITLE),
                 "Compared: {db1} <-> {db2}".format(**m),
                 "Filter: {filter}".format(**m),
                 "Run time: {start_time} -- {end_time} ({elapsed:.1f} sec)".format(**m),
                 ""]
        for section in result.keys():
            lines.append("* " + section.title())
            indent = " " * 4
            if len(result[section]) == 0:
                lines.append("{}EMPTY".format(indent))
            else:
                keyset, maxwid = self.result_subsets(result[section])
                for columns in keyset:
                    ocol = self.ordered_cols(columns, section)
                    mw = maxwid[columns]
                    mw_i = [columns.index(c) for c in ocol]  # reorder indexes
                    fmt = '  '.join(["{{:{:d}s}}".format(mw[i]) for i in mw_i])
                    lines.append("")
                    lines.append(indent + fmt.format(*ocol))
                    lines.append(indent + '-_' * (sum(mw)/2 + len(columns)))
                    rows = result[section]
                    self.sort_rows(rows, section)
                    for r in rows:
                        key = tuple(sorted(r.keys()))
                        if key == columns:
                            values = [str(r[k]) for k in ocol]
                            lines.append(indent + fmt.format(*values))
        return '\n'.join(lines)

    def _record(self, rec):
        fields = ['{}: {}'.format(k, v) for k, v in rec.items()]
        return '{' + ', '.join(fields) + '}'
