from pathlib import Path
from typing import Union, Tuple

from flask import Flask, Blueprint, send_from_directory
from google import auth, cloud

from .exceptions import NotFoundDestinationError
from .buckets import LocalBucket, CloudBucket, Bucket


class GoogleStorage:
    """
    This is the main extension class. Typically you should create one instance per application
    passing your defined buckets (instances of :py:class:`flask_googlestorage.Bucket`). Make sure
    you call :py:func:`init_app` before start using your buckets. If the application instance is
    given at creation time, :py:func:`init_app` is called for you.
    """

    def __init__(self, *buckets: Tuple[Bucket, ...], app: Flask = None):
        self.buckets = buckets

        if app is not None:
            self.init_app(app)

    def init_app(self, app: Flask):
        """
        Initialize the extension for the given :py:class:`flask.Flask` application instance

        :param app: The application instance
        """
        self._app = app
        self._prefix = "GOOGLE_STORAGE"

        try:
            uploads_dest = Path(self._app.config[f"{self._prefix}_LOCAL_DEST"])
        except KeyError:
            raise NotFoundDestinationError(
                "You must set the 'GOOGLE_STORAGE_LOCAL_DEST' configuration variable"
            )

        try:
            self._client = cloud.storage.Client()
        except auth.exceptions.DefaultCredentialsError:
            app.logger.warning("Could not authenticate the Google Cloud Storage client")
            self._client = None

        app.extensions = getattr(app, "extensions", {})
        ext = app.extensions.setdefault("googlestorage", {})
        ext["ext_obj"] = self
        ext["buckets"] = {}

        self._resolve_conflicts = app.config.get(f"{self._prefix}_RESOLVE_CONFLICTS", False)
        self._delete_local = app.config.get(f"{self._prefix}_DELETE_LOCAL", True)
        self._signature = app.config.get(f"{self._prefix}_SIGNATURE")
        self._tenacity = app.config.get(f"{self._prefix}_TENACITY")

        for bucket in self.buckets:
            ext["buckets"][bucket.name] = self._create_bucket(uploads_dest, bucket)

    def _create_bucket(self, uploads_dest: Path, bucket: Bucket) -> Union[LocalBucket, CloudBucket]:
        cfg = self._app.config
        prefix = f"{self._prefix}_{bucket.name.upper()}"

        destination = uploads_dest / bucket.name
        resolve_conflicts = cfg.get(f"{prefix}_RESOLVE_CONFLICTS", self._resolve_conflicts)

        cloud_bucket = None
        bucket_name = cfg.get(f"{prefix}_BUCKET")
        if self._client and bucket_name:
            delete_local = cfg.get(f"{prefix}_DELETE_LOCAL", self._delete_local)
            try:
                cloud_bucket = CloudBucket(
                    bucket.name,
                    self._client.get_bucket(bucket_name),
                    destination,
                    resolve_conflicts=resolve_conflicts,
                    delete_local=delete_local,
                    signature=cfg.get(f"{prefix}_SIGNATURE", self._signature),
                    tenacity=cfg.get(f"{prefix}_TENACITY", self._tenacity),
                )
                if not delete_local:
                    self._register_blueprint(bucket.name, destination)

                return cloud_bucket
            except cloud.exceptions.NotFound:
                self._app.logger.warning(f"Could not found the bucket for {bucket.name}")

        local_bucket = LocalBucket(bucket.name, destination, resolve_conflicts)
        self._register_blueprint(bucket.name, destination)

        return local_bucket

    def _register_blueprint(self, name, destination):
        bp = Blueprint(f"{name}_uploads", name, url_prefix=f"/_uploads/{name}")

        @bp.route("/<path:filename>")
        def download_file(filename):
            return send_from_directory(destination, filename)

        self._app.register_blueprint(bp)
