"""config.py

Terracotta settings parsing.
"""

from typing import Mapping, Any, Tuple, NamedTuple, Dict, List, Optional
import os
import json
import tempfile
import warnings

from marshmallow import Schema, fields, validate, pre_load, post_load, ValidationError

from terracotta import exceptions


class TerracottaSettings(NamedTuple):
    """Contains all settings for the current Terracotta instance."""

    #: Path to database
    DRIVER_PATH: str = ""

    #: Driver provider to use (sqlite, sqlite-remote, mysql, postgresql; auto-detected by default)
    DRIVER_PROVIDER: Optional[str] = None

    #: Activate debug mode in Flask app
    DEBUG: bool = False

    #: Print profile information after every request
    FLASK_PROFILE: bool = False

    #: Send performance traces to AWS X-Ray
    XRAY_PROFILE: bool = False

    #: Default log level (debug, info, warning, error, critical)
    LOGLEVEL: str = "warning"

    #: Size of raster file in-memory cache in bytes
    RASTER_CACHE_SIZE: int = 1024 * 1024 * 490  # 490 MB

    #: Compression level of raster file in-memory cache, from 0-9
    RASTER_CACHE_COMPRESS_LEVEL: int = 9

    #: Tile size to return if not given in parameters
    DEFAULT_TILE_SIZE: Tuple[int, int] = (256, 256)

    #: Maximum size to use when lazy loading metadata (less is faster but less accurate)
    LAZY_LOADING_MAX_SHAPE: Tuple[int, int] = (1024, 1024)

    #: Compression level of output PNGs, from 0-9
    PNG_COMPRESS_LEVEL: int = 1

    #: Timeout in seconds for database connections
    DB_CONNECTION_TIMEOUT: int = 10

    #: Path where cached remote SQLite databases are stored (when using sqlite-remote provider)
    REMOTE_DB_CACHE_DIR: str = os.path.join(tempfile.gettempdir(), "terracotta")

    #: Time-to-live of remote database cache in seconds
    REMOTE_DB_CACHE_TTL: int = 10 * 60  # 10 min

    #: Resampling method to use when reading reprojected data
    RESAMPLING_METHOD: str = "average"

    #: Resampling method to use when reprojecting data to Web Mercator
    REPROJECTION_METHOD: str = "linear"

    #: CORS allowed origins for metadata endpoint
    ALLOWED_ORIGINS_METADATA: List[str] = ["*"]

    #: CORS allowed origins for tiles endpoints
    ALLOWED_ORIGINS_TILES: List[str] = [r"http[s]?://(localhost|127\.0\.0\.1):*"]

    #: SQL database username (if not given in driver path)
    SQL_USER: Optional[str] = None

    #: SQL database password (if not given in driver path)
    SQL_PASSWORD: Optional[str] = None

    #: Deprecated, use SQL_USER. MySQL database username (if not given in driver path)
    MYSQL_USER: Optional[str] = None

    #: Deprecated, use SQL_PASSWORD. MySQL database password (if not given in driver path)
    MYSQL_PASSWORD: Optional[str] = None

    #: Deprecated, use SQL_USER. PostgreSQL database username (if not given in driver path)
    POSTGRESQL_USER: Optional[str] = None

    #: Deprecated, use SQL_PASSWORD. PostgreSQL database password (if not given in driver path)
    POSTGRESQL_PASSWORD: Optional[str] = None

    #: Use a process pool for band retrieval in parallel
    USE_MULTIPROCESSING: bool = True

    #: Maximum number of metadata keys per POST /metadata request
    MAX_POST_METADATA_KEYS: int = 100


AVAILABLE_SETTINGS: Tuple[str, ...] = TerracottaSettings._fields

DEPRECATION_MAP: Dict[str, str] = {
    # TODO: Remove in v0.8.0
    "MYSQL_USER": "SQL_USER",
    "MYSQL_PASSWORD": "SQL_PASSWORD",
    "POSTGRESQL_USER": "SQL_USER",
    "POSTGRESQL_PASSWORD": "SQL_PASSWORD",
}


def _is_writable(path: str) -> bool:
    return os.access(os.path.dirname(path) or os.getcwd(), os.W_OK)


class SettingSchema(Schema):
    """Schema used to create and validate TerracottaSettings objects"""

    DRIVER_PATH = fields.String()
    DRIVER_PROVIDER = fields.String(allow_none=True)

    DEBUG = fields.Boolean()
    FLASK_PROFILE = fields.Boolean()
    XRAY_PROFILE = fields.Boolean()

    LOGLEVEL = fields.String(
        validate=validate.OneOf(["debug", "info", "warning", "error", "critical"])
    )

    RASTER_CACHE_SIZE = fields.Integer(validate=validate.Range(min=0))
    RASTER_CACHE_COMPRESS_LEVEL = fields.Integer(validate=validate.Range(min=0, max=9))

    DEFAULT_TILE_SIZE = fields.List(fields.Integer(), validate=validate.Length(equal=2))

    LAZY_LOADING_MAX_SHAPE = fields.List(
        fields.Integer(validate=validate.Range(min=0)),
        validate=validate.Length(equal=2),
    )

    PNG_COMPRESS_LEVEL = fields.Integer(validate=validate.Range(min=0, max=9))

    DB_CONNECTION_TIMEOUT = fields.Integer(validate=validate.Range(min=0))
    REMOTE_DB_CACHE_DIR = fields.String(validate=_is_writable)
    REMOTE_DB_CACHE_TTL = fields.Integer(validate=validate.Range(min=0))

    RESAMPLING_METHOD = fields.String(
        validate=validate.OneOf(["nearest", "linear", "cubic", "average"])
    )
    REPROJECTION_METHOD = fields.String(
        validate=validate.OneOf(["nearest", "linear", "cubic", "average"])
    )

    ALLOWED_ORIGINS_METADATA = fields.List(fields.String())
    ALLOWED_ORIGINS_TILES = fields.List(fields.String())

    SQL_USER = fields.String(allow_none=True)
    SQL_PASSWORD = fields.String(allow_none=True)

    MYSQL_USER = fields.String(allow_none=True)
    MYSQL_PASSWORD = fields.String(allow_none=True)
    POSTGRESQL_USER = fields.String(allow_none=True)
    POSTGRESQL_PASSWORD = fields.String(allow_none=True)

    USE_MULTIPROCESSING = fields.Boolean()

    MAX_POST_METADATA_KEYS = fields.Integer(validate=validate.Range(min=1))

    @pre_load
    def decode_lists(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
        for var in (
            "DEFAULT_TILE_SIZE",
            "LAZY_LOADING_MAX_SHAPE",
            "ALLOWED_ORIGINS_METADATA",
            "ALLOWED_ORIGINS_TILES",
        ):
            val = data.get(var)
            if val and isinstance(val, str):
                try:
                    data[var] = json.loads(val)
                except json.decoder.JSONDecodeError as exc:
                    raise ValidationError(
                        f'Could not parse value for key {var} as JSON: "{val}"'
                    ) from exc
        return data

    @pre_load
    def handle_deprecated_fields(
        self, data: Dict[str, Any], **kwargs: Any
    ) -> Dict[str, Any]:
        for deprecated_field, new_field in DEPRECATION_MAP.items():
            if data.get(deprecated_field):
                warnings.warn(
                    f"Setting TC_{deprecated_field} is deprecated "
                    "and will be removed in the next major release. "
                    f"Please use TC_{new_field} instead.",
                    exceptions.DeprecationWarning,
                )

                # Only use the mapping if the new field has not been set
                if not data.get(new_field):
                    data[new_field] = data[deprecated_field]

        return data

    @post_load
    def make_settings(self, data: Dict[str, Any], **kwargs: Any) -> TerracottaSettings:
        # encode tuples
        for var in (
            "DEFAULT_TILE_SIZE",
            "LAZY_LOADING_MAX_SHAPE",
            "ALLOWED_ORIGINS_METADATA",
            "ALLOWED_ORIGINS_TILES",
        ):
            val = data.get(var)
            if val:
                data[var] = tuple(val)

        return TerracottaSettings(**data)


def parse_config(config: Optional[Mapping[str, Any]] = None) -> TerracottaSettings:
    """Parse given config dict and return new TerracottaSettings object"""
    config_dict = dict(config or {})

    for setting in AVAILABLE_SETTINGS:
        env_setting = f"TC_{setting}"
        if setting not in config_dict and env_setting in os.environ:
            config_dict[setting] = os.environ[env_setting]

    schema = SettingSchema()
    try:
        new_settings = schema.load(config_dict)
    except ValidationError as exc:
        raise ValueError("Could not parse configuration") from exc

    return new_settings
