#!/usr/bin/python3

import os
import sys
import logging
import cmdln

from fnmatch import fnmatch
from configparser import ConfigParser
import solv

logger = None

REASONS = dict([(getattr(solv.Solver, i), i[14:]) for i in dir(solv.Solver) if i.startswith('SOLVER_REASON_')])


class DepTool(cmdln.Cmdln):
    def __init__(self, *args, **kwargs):
        cmdln.Cmdln.__init__(self, args, kwargs)

    def get_optparser(self):
        parser = cmdln.CmdlnOptionParser(self)
        parser.add_option("--dry", action="store_true", help="dry run")
        parser.add_option("--debug", action="store_true", help="debug output")
        parser.add_option("--verbose", action="store_true", help="verbose")
        parser.add_option("--system", action="store_true", help="with system repo")
        parser.add_option("--arch", dest="arch", help="architecture", default='x86_64')
        return parser

    def postoptparse(self):
        level = None
        if self.options.debug:
            level = logging.DEBUG
        elif self.options.verbose:
            level = logging.INFO

        logging.basicConfig(level=level)

        global logger
        logger = logging.getLogger()

    def prepare_pool(self, repos):

        self.pool = solv.Pool()
        self.pool.setarch(self.options.arch)

        self._read_repos(repos)

        if self.options.system:
            self._add_system_repo()

        self.pool.addfileprovides()
        self.pool.createwhatprovides()

    def _read_repos(self, repos):
        repodir = '/etc/zypp/repos.d'
        solvfile = '/var/cache/zypp/solv/%s/solv'
        onlyenabled = False

        parser = ConfigParser()

        if not repos:
            repos = [f for f in os.listdir(repodir) if fnmatch(f, '*.repo')]
            onlyenabled = True

        for r in repos:
            if '/' in r or r.endswith('.solv'):
                name = os.path.basename(os.path.splitext(r)[0])
                repo = self.pool.add_repo(name)
                repo.add_solv(r)
                logger.debug(f"add repo {name}")
            else:
                try:
                    if r.endswith('.repo'):
                        name = os.path.splitext(r)[0]
                    else:
                        name = r
                        r += '.repo'
                    parser.read('/'.join((repodir, r)))
                    if onlyenabled is False or parser.get(name, 'enabled') == '1':
                        repo = self.pool.add_repo(name)
                        repo.add_solv(solvfile % name)
                        if parser.has_option(name, 'priority'):
                            repo.priority = parser.getint(name, 'priority')
                        logger.debug(f"add repo {name}")
                except Exception as e:
                    logger.error(e)

    def _add_system_repo(self):
        solvfile = '/var/cache/zypp/solv/@System/solv'
        repo = self.pool.add_repo('system')
        repo.add_solv(solvfile)

    @cmdln.option("-s", "--single", action="store_true",
                  help="single step all requires/recommends")
    @cmdln.option("--size", action="store_true",
                  help="print installed size")
    @cmdln.option("-l", "--lock", dest="lock", action="append",
                  help="packages to lock")
    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    @cmdln.option("--explain", dest="explain", action="append",
                  help="rule to explain")
    @cmdln.option("--solver-debug", action="store_true",
                  help="debug solver")
    @cmdln.option("--ignore-recommended", action="store_true",
                  help="ignore recommended")
    def do_install(self, subcmd, opts, *args):
        """${cmd_name}: generate pot file for patterns

        ${cmd_usage}
        ${cmd_option_list}
        """

        locked = []
        if opts.lock:
            for list in opts.lock:
                for lock in list.split(','):
                    locked.append(lock)

        good = True

        self.prepare_pool(opts.repo)
        if opts.solver_debug:
            self.pool.set_debuglevel(3)

        def solveit(packages):
            jobs = []
            for lock in locked:
                sel = self.pool.select(str(lock), solv.Selection.SELECTION_NAME)
                if sel.isempty():
                    # if we can't find it, it probably is not as important
                    logger.debug(f'locked package {lock} not found')
                else:
                    jobs += sel.jobs(solv.Job.SOLVER_LOCK)

            for n in packages:
                sel = self.pool.select(str(n), solv.Selection.SELECTION_NAME)
                if sel.isempty():
                    logger.error(f'package {n} not found')
                jobs += sel.jobs(solv.Job.SOLVER_INSTALL)

            solver = self.pool.Solver()

            if opts.ignore_recommended:
                solver.set_flag(solver.SOLVER_FLAG_IGNORE_RECOMMENDED, 1)

            problems = solver.solve(jobs)
            if problems:
                for problem in problems:
                    logger.error('%s', problem)
                return False

            trans = solver.transaction()
            if trans.isempty():
                logger.error('nothing to do')
                return False

            for s in trans.newsolvables():
                print(','.join(packages), s.name)
                if opts.explain and s.name in opts.explain:
                    reason, rule = solver.describe_decision(s)
                    ruleinfo = None
                    if rule:
                        ruleinfo = rule.info().problemstr()
                    if reason == solv.Solver.SOLVER_REASON_WEAKDEP:
                        for v in solver.describe_weakdep_decision(s):
                            reason2, s2, dep = v
                            print(f"-> {s2.name} {REASONS[reason2]} {dep}")
                    else:
                        print(f"-> {s.name} {REASONS[reason]} {ruleinfo}")

            if opts.size:
                size = trans.calc_installsizechange()
                print(f"SIZE {size}")

            return True

        if opts.single:
            for n in args:
                sel = self.pool.select(str(n), solv.Selection.SELECTION_NAME)
                for s in sel.solvables():
                    deps = s.lookup_deparray(solv.SOLVABLE_RECOMMENDS)
                    deps += s.lookup_deparray(solv.SOLVABLE_REQUIRES)
                    for dep in deps:
                        # only add recommends that exist as packages
                        rec = self.pool.select(dep.str(), solv.Selection.SELECTION_NAME)
                        if not rec.isempty():
                            if not solveit([dep.str()]):
                                good = False
        else:
            if not solveit(args):
                good = False

        if not good:
            logger.error("solver errors encountered")
            return 1

    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_deps(self, subcmd, opts, *packages):
        """${cmd_name}: show package deps

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        for n in packages:
            sel = self.pool.select(n, solv.Selection.SELECTION_NAME)
            if sel.isempty():
                logger.error("%s not found", n)
            for s in sel.solvables():
                print(f'- {s.name}-{s.evr}@{s.arch}:')
                for kind in ('RECOMMENDS', 'REQUIRES', 'SUPPLEMENTS', 'ENHANCES', 'PROVIDES', 'SUGGESTS'):
                    deps = s.lookup_deparray(getattr(solv, 'SOLVABLE_' + kind), 0)
                    if deps:
                        print(f'  {kind}:')
                        for dep in deps:
                            print(f'    - {dep}')

    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_whatprovides(self, subcmd, opts, *relation):
        """${cmd_name}: list packages providing given relations

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        for r in relation:
            i = self.pool.str2id(r)
            for s in self.pool.whatprovides(i):
                print(f'- {s.name}-{s.evr}@{s.arch}:')

    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_patterns(self, subcmd, opts, *relation):
        """${cmd_name}: list patterns

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        patternid = self.pool.str2id('pattern()')
        for s in self.pool.whatprovides(patternid):
            deps = s.lookup_deparray(solv.SOLVABLE_PROVIDES)
            order = 0
            for dep in deps:
                name = str(dep)
                if name.startswith('pattern-order()'):
                    # XXX: no function in bindings to do that properly
                    order = name[name.find('= ') + 2:]
            print(f"{order} {s.name}")

    @cmdln.option("--providers", action="store_true",
                  help="also show other providers")
    @cmdln.option("--relation", action="store_true",
                  help="arguments are relations rather than package names")
    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_rdeps(self, subcmd, opts, *args):
        """${cmd_name}: list packages that require, recommend etc the given packages

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        kinds = ['RECOMMENDS', 'REQUIRES', 'SUPPLEMENTS', 'ENHANCES', 'SUGGESTS']
        if opts.providers:
            kinds.append('PROVIDES')

        for kind in kinds:
            kindid = getattr(solv, 'SOLVABLE_' + kind, 0)
            kindprinted = False
            if opts.relation:
                # FIXME: doesnt work
                for r in args:
                    sel = self.pool.matchdeps(r, solv.Selection.SELECTION_REL | solv.Selection.SELECTION_FLAT, kindid)
                    if sel.isempty():
                        logger.info('nothing %s %s', kind.lower(), r)
                        continue
                    for s in sel.solvables():
                        print(f'  {r}: {s.name}-{s.evr}@{s.arch}')
            else:
                for n in args:
                    sel = self.pool.select(n, solv.Selection.SELECTION_NAME)
                    if sel.isempty():
                        logger.error("%s not found", n)
                        continue
                    for s in sel.solvables():
                        prov = s.lookup_deparray(solv.SOLVABLE_PROVIDES, 0)
                        if not prov:
                            logger.error("%s doesn't provide anything")
                            continue
                        for p in prov:
                            sel = self.pool.matchdepid(p, solv.Selection.SELECTION_REL |
                                                       solv.Selection.SELECTION_FLAT, kindid)
                            if sel.isempty():
                                logger.debug('nothing %s %s', kind.lower(), p)
                                continue
                            for r in sel.solvables():
                                if kindid == solv.SOLVABLE_PROVIDES and r == s:
                                    continue
                                if not kindprinted:
                                    print(kind)
                                    kindprinted = True
                                print(f'  {p}: {r.name}-{r.evr}@{r.arch}')

    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_what(self, subcmd, opts, *relation):
        """${cmd_name}: list packages that have dependencies on given relation

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        kinds = ['PROVIDES', 'RECOMMENDS', 'REQUIRES', 'SUPPLEMENTS', 'ENHANCES', 'SUGGESTS']

        for r in relation:
            p = self.pool.str2id(r)
            for kind in kinds:
                kindprinted = False
                kindid = getattr(solv, 'SOLVABLE_' + kind, 0)
                sel = self.pool.matchdepid(p, solv.Selection.SELECTION_REL | solv.Selection.SELECTION_FLAT, kindid)
                if sel.isempty():
                    logger.debug('nothing %s %s', kind.lower(), p)
                    continue
                for r in sel.solvables():
                    if not kindprinted:
                        print(kind)
                        kindprinted = True
                    print(f'  {r.name}-{r.evr}@{r.arch}')

    @cmdln.option("-r", "--repo", dest="repo", action="append",
                  help="repo to use")
    def do_info(self, subcmd, opts, *args):
        """${cmd_name}: show some info about a package

        ${cmd_usage}
        ${cmd_option_list}
        """

        self.prepare_pool(opts.repo)

        sattrs = [s for s in dir(solv) if s.startswith("SOLVABLE_")]
        for n in args:
            sel = self.pool.select(str(n), solv.Selection.SELECTION_NAME)
            for s in sel.solvables():
                for attr in sattrs:
                    sid = getattr(solv, attr, 0)
                    # pretty stupid, just lookup strings
                    value = s.lookup_str(sid)
                    if value:
                        print(f"{attr[len('SOLVABLE_'):]}: {value}")


if __name__ == "__main__":
    app = DepTool()
    sys.exit(app.main())
else:
    logger = logging.getLogger()
