#!/usr/bin/env python
#
# ELBE - Debian Based Embedded Rootfilesystem Builder
# Copyright (C) 2013  Linutronix GmbH
#
# This file is part of ELBE.
#
# ELBE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ELBE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ELBE.  If not, see <http://www.gnu.org/licenses/>.

import os
import errno
import re

from datetime import datetime
from shutil import (rmtree, copyfile, copyfileobj)
from contextlib import contextmanager
from urllib import quote

from passlib.hash import pbkdf2_sha512

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (Column, ForeignKey)
from sqlalchemy import (Integer, String, Boolean, Sequence, DateTime)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session, object_mapper
from sqlalchemy.orm import relationship
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import OperationalError

from elbepack.elbeproject import E
lbeProject
from elbepack.elbexml import (ElbeXML, ValidationError)
from elbepack.dosu
nix import dos2unix

Base = declarative_base ()

class ElbeDBError(Exception):
    def __init__ (self, message):
        Exception.__init__(self, message)

class InvalidLogin(Exception):
    def __init__ (self):
        Exception.__init__(self, "Invalid login")

@contextmanager
def session_scope(session):
    try:
        yield session
        try:
            session.commit()
        except OperationalError as e:
            raise ElbeDBError( "database commit failed: " + str(e) )
    except:
        session.rollback()
        raise
    finally:
        session.remove()

def get_versioned_filename( name, version, suffix ):
    return quote( name, ' ') + '_' + quote( version, ' ') + suffix

class ElbeDB(object):
    db_path     = '/var/cache/elbe'
    db_location = 'sqlite:///' + db_path + '/elbe.db'

    def __init__ (self):
        engine = create_engine( self.__class__.db_location,
                connect_args={ 'timeout': 30 } )
        Base.metadata.create_all( engine )
        smaker = sessionmaker( bind=engine )
        self.session = scoped_session( smaker )

    def list_users (self):
        with session_scope(self.session) as s:
            res = s.query (User).all()
            ret = []
            for u in res:
                ret.append(UserData(u))
            return ret

    def list_projects (self):
        with session_scope(self.session) as s:
            res = s.query (Project).all()
            ret = []
            for p in res:
                ret.append(ProjectData(p))
            return ret

    def list_projects_of (self, userid):
        with session_scope(self.session) as s:
            res = s.query (Project).filter (Project.owner_id == userid).all()
            ret = []
            for p in res:
                ret.append(ProjectData(p))
            return ret

    def get_project_data (self, builddir):
        # Can throw: ElbeDBError
        if not os.path.exists (builddir):
            raise ElbeDBError( "project directory does not exist" )

        with session_scope(self.session) as s:
            try:
                p = s.query (Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                        "project %s is not registered in the database" %
                        builddir )

            return ProjectData(p)

    def set_postbuild (self, builddir, postbuild_file):
        if not os.path.exists (builddir):
            raise ElbeDBError( "project directory does not exist" )

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query (Project). \
                        filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                        "project %s is not registered in the database" %
                        builddir )

            if p.status == "busy":
                raise ElbeDBError(
                        "cannot set postbuild file while project %s is busy" %
                        builddir )

            p.edit = datetime.utcnow ()

            with open (builddir+"/postbuild.sh", 'w') as dst:
                copyfileobj (postbuild_file, dst)

            os.chmod (builddir+"/postbuild.sh", 0755)
            dos2unix (builddir+"/postbuild.sh")

            return self._update_project_file( s, builddir, "postbuild.sh",
                    "application/sh", "postbuild script" )

    def set_savesh (self, builddir, savesh_file):
        if not os.path.exists (builddir):
            raise ElbeDBError( "project directory does not exist" )

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query (Project). \
                        filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                        "project %s is not registered in the database" %
                        builddir )

            if p.status == "busy":
                raise ElbeDBError(
                        "cannot set savesh file while project %s is busy" %
                        builddir )

            p.edit = datetime.utcnow ()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            with open (builddir+"/save.sh", 'w') as dst:
                copyfileobj (savesh_file, dst)

            os.chmod (builddir+"/save.sh", 0755)
            dos2unix (builddir+"/save.sh")

            return self._update_project_file( s, builddir, "save.sh",
                    "application/sh", "version save script" )

    def set_presh (self, builddir, presh_file):
        if not os.path.exists (builddir):
            raise ElbeDBError( "project directory does not exist" )

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query (Project). \
                        filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                        "project %s is not registered in the database" %
                        builddir )

            if p.status == "busy":
                raise ElbeDBError(
                        "cannot set presh file while project %s is busy" %
                        builddir )

            p.edit = datetime.utcnow ()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            with open (builddir+"/pre.sh", 'w') as dst:
                copyfileobj (presh_file, dst)

            dos2unix (builddir+"/pre.sh")

            return self._update_project_file( s, builddir, "pre.sh",
                    "application/sh", "pre install script" )

    def set_postsh (self, builddir, postsh_file):
        if not os.path.exists (builddir):
            raise ElbeDBError( "project directory does not exist" )

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query (Project). \
                        filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                        "project %s is not registered in the database" %
                        builddir )

            if p.status == "busy":
                raise ElbeDBError(
                        "cannot set postsh file while project %s is busy" %
 