#!/usr/bin/python

# Copyright (C) 2012, Benjamin Drung <bdrung@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""Validates a given Debian or Ubuntu distro-info CSV file."""

import csv
import datetime
import optparse
import os
import sys

_COLUMNS = {
    "debian": ("version", "codename", "series", "created", "release", "eol"),
    "ubuntu": ("version", "codename", "series", "created", "release", "eol",
               "eol-server"),
}
_DATES = ("created", "release", "eol", "eol-server")
_EARLIER_DATES = (
    ("created", "release"),
    ("release", "eol"),
    ("eol", "eol-server"),
)
_STRINGS = {
    "debian": ("codename", "series"),
    "ubuntu": ("version", "codename", "series"),
}

def convert_date(string):
    """Convert a date string in ISO 8601 into a datetime object."""
    if not string:
        date = None
    else:
        parts = [int(x) for x in string.split("-")]
        if len(parts) == 3:
            (year, month, day) = parts
            date = datetime.date(year, month, day)
        else:
            raise ValueError("Date not in ISO 8601 format.")
    return date

def error(filename, line, message, *args):
    """Prints an error message"""
    print >> sys.stderr, "%s:%i: %s." % (filename, line, message % args)

def validate(filename, distro):
    """Validates a given CSV file.

    Returns True if the given CSV file is valid and otherwise False.
    """
    failures = 0
    content = open(filename).readlines()
    # Remove comments
    for line in xrange(len(content)):
        if content[line].startswith("#"):
            content[line] = "\n"
    csvreader = csv.DictReader(content)
    for row in csvreader:
        # Check for missing columns
        for column in _COLUMNS[distro]:
            if not column in row:
                msg = "Column `%s' is missing"
                error(filename, csvreader.line_num, msg, column)
                failures += 1
        # Check for additinal columns
        for column in row:
            if not column in _COLUMNS[distro]:
                msg = "Additional column `%s' is specified"
                error(filename, csvreader.line_num, msg, column)
                failures += 1
        # Check required strings columns
        for column in _STRINGS[distro]:
            if column in row and not row[column]:
                msg = "Empty column `%s' specified"
                error(filename, csvreader.line_num, msg, column)
                failures += 1
        # Check dates
        for column in _DATES:
            if column in row:
                try:
                    row[column] = convert_date(row[column])
                except ValueError:
                    msg = "Invalid date `%s' in column `%s'"
                    error(filename, csvreader.line_num, msg, row[column],
                           column)
                    failures += 1
                    row[column] = None
        # Check required date columns
        column = "created"
        if column in row and not row[column]:
            msg = "No date specified in column `%s'"
            error(filename, csvreader.line_num, msg, column)
            failures += 1
        # Compare dates
        for (date1, date2) in _EARLIER_DATES:
            if date2 in row and row[date2]:
                if date1 in row and row[date1]:
                    # date1 needs to be earlier than date2
                    if row[date1] >= row[date2]:
                        msg = ("Date %s of column `%s' needs to be later "
                               "than %s of column `%s'")
                        error(filename, csvreader.line_num, msg,
                              row[date2].isoformat(), date2,
                              row[date1].isoformat(), date1)
                        failures += 1
                else:
                    # date1 needs to be specified if date1 is specified
                    msg = ("A date needs to be specified in column `%s' due "
                           "to the given date in column `%s'")
                    error(filename, csvreader.line_num, msg, date1, date2)
                    failures += 1

    return failures == 0

def main():
    """Main function with command line parameter parsing."""
    script_name = os.path.basename(sys.argv[0])
    usage = "%s -d|-u csv-file" % (script_name)
    parser = optparse.OptionParser(usage=usage)

    parser.add_option("-d", "--debian", dest="debian", action="store_true",
                      default=False, help="validate a Debian CSV file")
    parser.add_option("-u", "--ubuntu", dest="ubuntu", action="store_true",
                      default=False, help="validate an Ubuntu CSV file")

    (options, args) = parser.parse_args()

    if len(args) == 0:
        parser.error("No CSV file specified.")
    elif len(args) > 1:
        parser.error("Multiple CSV files specified: %s" % (", ".join(args)))

    if len([x for x in [options.debian, options.ubuntu] if x]) != 1:
        parser.error("You have to select exactly one of --debian, --ubuntu.")
    if options.debian:
        distro = "debian"
    else:
        distro = "ubuntu"

    if validate(args[0], distro):
        return 0
    else:
        return 1

if __name__ == "__main__":
    sys.exit(main())
