""" base class for image download """
import os
import re
import json
import time
import hashlib
import datetime
import configparser

import requests

class FetchDistroCore:
    """ base class for image download """

    arch = os.uname().machine
    retries = 5
    timeout = 20

    def __init__(self, arch = None):
        if arch is not None:
            self.arch = arch

    @staticmethod
    def index_load(imgdir):
        try:
            with open(f'{imgdir}/index.json', 'r', encoding = 'utf-8') as f:
                index = json.load(f)
            return index
        except FileNotFoundError:
            return {}

    @staticmethod
    def index_dump(imgdir, index):
        with open(f'{imgdir}/index.json', 'w', encoding = 'utf-8') as f:
            json.dump(index, f)

    @staticmethod
    def checksum_dump(imgdir, index, hashalgo = 'sha256'):
        with open(f'{imgdir}/checksums.{hashalgo}', 'w', encoding = 'utf-8') as f:
            for name in index.keys():
                if index.get(name).get('hashalgo') != hashalgo:
                    continue
                filename = index.get(name).get('filename')
                checksum = index.get(name).get('checksum')
                print(f'{checksum} {filename}', file = f)

    @staticmethod
    def index_update(imgdir, image_id, filename, checksum, hashalgo = 'sha256'):
        index = FetchDistroCore.index_load(imgdir)
        index[image_id] = { 'filename' : filename,
                            'checksum' : checksum,
                            'hashalgo' : hashalgo }
        FetchDistroCore.index_dump(imgdir, index)
        FetchDistroCore.checksum_dump(imgdir, index, hashalgo)

    @staticmethod
    def index_find(imgdir, image_id):
        index = FetchDistroCore.index_load(imgdir)
        entry = index.get(image_id)
        if entry is None:
            return None
        filename = entry.get('filename')
        if filename is None:
            return None
        return f'{imgdir}/{filename}'

    @staticmethod
    def request(url, stream, start = None, end = None):
        hdr = {}
        if start and end:
            hdr['range'] = f'bytes={start}-{end-1}'
        r = FetchDistroCore.retries
        for i in range(r):
            try:
                req = requests.get(url, headers = hdr,
                                   stream = stream,
                                   timeout = FetchDistroCore.timeout)
            except requests.exceptions.RequestException as e:
                print(f'# request connect error [{i}/{r}]: {e}')
                print(f'# retry in {FetchDistroCore.timeout} seconds')
                time.sleep(FetchDistroCore.timeout)
                continue
            req.raise_for_status()
            if i > 0:
                print(f'# ... connected [{i}/{r}]')
            return req
        return None

    @staticmethod
    def fetch_image_checksum(url, out, checksum):
        """ download one file, verify checksum """

        print(f'# latest: {out}')
        print(f'# sha256: {checksum}')
        if os.path.exists(out):
            print('# skip download, file exists')
            return
        print('# downloading file ...')

        req = FetchDistroCore.request(url, True)
        if req is None:
            raise RuntimeError('connect failed')
        count = 0
        length = int(req.headers.get("content-length", 0))
        lengthm = int(length / (1024*1024))
        hasher = hashlib.sha256()

        with open(out, 'wb') as fh:
            i = req.iter_content(chunk_size = 1024 * 64)
            while True:
                try:
                    chunk = next(i)
                except StopIteration:
                    chunk = None
                except requests.exceptions.RequestException as e:
                    print('')
                    print(f'# request read error: {e}')
                    print('# try reconnect ...')
                    req = FetchDistroCore.request(url, True, count, length)
                    if req is None:
                        os.remove(out)
                        raise RuntimeError('re-connect failed') from e
                    i = req.iter_content(chunk_size = 1024 * 64)
                    continue

                if chunk is None:
                    break
                fh.write(chunk)
                hasher.update(chunk)
                count += len(chunk)
                countm = int(count / (1024*1024))
                if length == 0:
                    print(f'# {countm:4}M', end='\r')
                else:
                    percent = int(count * 100 / length)
                    print(f'# {countm:4}M / {lengthm:4}M - {percent:2}%', end='\r')

        print()
        if hasher.hexdigest() != checksum:
            print(f'# sha256: {hasher.hexdigest()} (mismatch)')
            os.remove(out)
            raise RuntimeError('sha256 mismatch')

        print('# sha256: check ok')

    @staticmethod
    def checksum_find_latest(url, pattern, checksum = 'SHA256'):
        """ find file (and sha256) by downloading CHECKSUM file """

        req = FetchDistroCore.request(url, False)
        if req is None:
            raise RuntimeError('request failed')
        matches = []
        for line in req.text.splitlines():
            if not line.startswith(checksum):
                continue
            if not pattern in line:
                continue
            if 'latest' in line:
                continue
            matches.append(line)
        if len(matches) == 0:
            return (None, None)
        latest = sorted(matches, reverse = True)[0]
        regex = checksum + r' \((.+)\) = ([a-fA-F0-9]{64})'
        m = re.match(regex, latest)
        filename = m.group(1)
        checksum = m.group(2)
        return (filename, checksum)

    @staticmethod
    def find_mirror(url):
        """ find location by following mirror network redirects """
        retries = 5
        while retries > 0:
            print(f'# {url}')
            r = requests.head(url, allow_redirects = False,
                              timeout = FetchDistroCore.timeout)
            if r.status_code == 200:
                print('# -> OK')
                return url
            if r.status_code in (301, 302):
                print(f'# -> Redirect [{r.status_code}]')
                url = r.headers['location']
            else:
                print(f'# -> Oops [{r.status_code}]')
                return None
            retries -= 1
        print('# too many redirects')
        return None

class TreeInfo:
    """ handle .treeinfo files """

    def __init__(self, url):
        self.info = configparser.ConfigParser()
        req = FetchDistroCore.request(url, False)
        if req is None:
            raise RuntimeError('request failed')
        self.info.read_string(req.text)

    def sha256(self, image):
        line = self.info.get('checksums', image)
        if not line:
            return None
        (algo, checksum) = line.split(':')
        if algo == 'sha256':
            return checksum
        return None

    def release_date(self):
        ts = self.info.get('general', 'timestamp')
        date = datetime.date.fromtimestamp(int(ts))
        return date.isoformat()

    def distro_name(self):
        name = self.info.get('release', 'short').lower()
        version = self.info.get('release', 'version').lower()
        return f'{name}-{version}'

class FetchNetinst(FetchDistroCore):
    """ class for fedora / centos / rhel netinst iso download """

    def __init__(self, mirror, arch = None):
        super().__init__(arch)
        self.mirror = mirror
        # filled by treeinfo()
        self.info = None
        self.netinst = None
        self.outfile = None
        self.image_id = None
        self.checksum = None

    def baseurl(self, _release):
        return None

    def treeinfo(self, release):
        # pylint: disable=assignment-from-none
        base = self.baseurl(release)
        boot = 'images/boot.iso'
        self.info = TreeInfo(f'{base}.treeinfo')
        self.netinst = f'{base}{boot}'
        self.checksum = self.info.sha256(boot)
        reldate = self.info.release_date()
        distname = self.info.distro_name()
        self.outfile = f'{distname}-{self.arch}-netinst-{reldate}.iso'
        self.image_id = f'{distname}-{self.arch}-netinst'

    def fetch_image(self, release, imgdir = 'distro'):
        self.treeinfo(release)
        print(f'# ----- {self.outfile} -----')
        url = self.find_mirror(self.netinst)
        out = f'{imgdir}/{self.outfile}'
        os.makedirs(imgdir, exist_ok = True)
        self.fetch_image_checksum(url, out, self.checksum)
        self.index_update(imgdir, self.image_id, self.outfile, self.checksum)
