from os import environ
import logging
from pathlib import Path
from typing import Any, Type, Tuple

from pydantic_settings import (BaseSettings,
                               PydanticBaseSettingsSource,
                               SettingsConfigDict)
from pydantic import ConfigDict
from pydantic.v1.utils import deep_update
from yaml import safe_load

from gisaf._version import __version__
#from sqlalchemy.ext.asyncio.engine import AsyncEngine
#from sqlalchemy.orm.session import sessionmaker

logger = logging.getLogger(__name__)
ENV = environ.get('env', 'prod')

config_files = [
    Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV,
    Path.home() / '.local' / 'gisaf' / ENV
]

class DashboardHome(BaseSettings):
    title: str
    content_file: str
    footer_file: str

class GisafConfig(BaseSettings):
    title: str
    windowTitle: str
    debugLevel: str
    dashboard_home: DashboardHome
    redirect: str = ''
    use_pretty_errors: bool = False

class SpatialSysRef(BaseSettings):
    author: str
    ellps: str
    k: int
    lat_0: float
    lon_0: float
    no_defs: bool
    proj: str
    towgs84: str
    units: str
    x_0: float
    y_0: float

class RawSurvey(BaseSettings):
    spatial_sys_ref: SpatialSysRef
    srid: int

class Geo(BaseSettings):
    raw_survey: RawSurvey
    simplify_geom_factor: int
    srid: int
    srid_for_proj: int

class Flask(BaseSettings):
    secret_key: str
    debug: int

class MQTT(BaseSettings):
    broker: str = 'localhost'
    port: int = 1883

class GisafLive(BaseSettings):
    hostname: str
    port: int
    scheme: str
    redis: str
    mqtt: MQTT

class DefaultSurvey(BaseSettings):
    surveyor_id: int
    equipment_id: int

class Survey(BaseSettings):
    model_config = ConfigDict(extra='ignore')
    db_schema_raw: str
    db_schema: str
    default: DefaultSurvey

class Crypto(BaseSettings):
    secret: str
    algorithm: str
    expire: float

class DB(BaseSettings):
    uri: str
    host: str
    port: int = 5432
    user: str
    db: str
    password: str
    debug: bool
    info: bool
    pool_size: int = 10
    max_overflow: int = 10
    echo: bool = False

    def get_sqla_url(self):
        return f'postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}'

    def get_pg_url(self):
        return f'postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}'


class Log(BaseSettings):
    level: str

class OGCAPILicense(BaseSettings):
    name: str
    url: str

class OGCAPIProvider(BaseSettings):
    name: str
    url: str

class OGCAPIServerContact(BaseSettings):
    name: str
    address: str
    city: str
    stateorprovince: str
    postalcode: int
    country: str
    email: str

class OGCAPIIdentification(BaseSettings):
    title: str
    description: str
    keywords: list[str]
    keywords_type: str
    terms_of_service: str
    url: str

class OGCAPIMetadata(BaseSettings):
    identification: OGCAPIIdentification
    license: OGCAPILicense
    provider: OGCAPIProvider
    contact: OGCAPIServerContact

class ServerBind(BaseSettings):
    host: str
    port: int

class OGCAPIServerMap(BaseSettings):
    url: str
    attribution: str

class OGCAPIServer(BaseSettings):
    bind: ServerBind
    url: str
    mimetype: str
    encoding: str
    language: str
    pretty_print: bool
    limit: int
    map: OGCAPIServerMap

class OGCAPI(BaseSettings):
    base_url: str
    bbox: list[float]
    log: Log
    metadata: OGCAPIMetadata
    server: OGCAPIServer

class TileServer(BaseSettings):
     baseDir: str
     useRequestUrl: bool = False
     spriteBaseDir: str
     spriteUrl: str
     spriteBaseUrl: str
     openMapTilesKey: str | None = None

class Map(BaseSettings):
    tileServer: TileServer | None = None
    zoom: int
    pitch: int
    lat: float
    lng: float
    bearing: float
    style: str
    opacity: float
    attribution: str
    status: list[str]
    defaultStatus: list[str] # FIXME: should be str
    tagKeys: list[str]

class Measures(BaseSettings):
    defaultStore: str

class BasketDefault(BaseSettings):
    surveyor: str
    equipment: str
    project: str | None
    status: str
    store: str | None

class BasketOldDef(BaseSettings):
    base_dir: str

class Basket(BaseSettings):
    base_dir: str
    default: BasketDefault

class Plot(BaseSettings):
    maxDataSize: int

class Dashboard(BaseSettings):
    base_source_url: str
    base_storage_dir: str
    base_storage_url: str

class Widgets(BaseSettings):
    footer: str

class Admin(BaseSettings):
    basket: Basket

class Attachments(BaseSettings):
    base_dir: str

class Job(BaseSettings):
    id: str
    func: str
    trigger: str
    minutes: int | None = 0
    seconds: int | None = 0

class Crs(BaseSettings):
    '''
    Handy definitions for crs-es
    '''
    db: str
    geojson: str
    for_proj: str
    survey: str
    web_mercator: str

class Config(BaseSettings):
    model_config = SettingsConfigDict(
        #env_prefix='gisaf_',
        env_nested_delimiter='__',
    )

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: Type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> Tuple[PydanticBaseSettingsSource, ...]:
        return env_settings, init_settings, file_secret_settings, config_file_settings

    # def __init__(self, **kwargs):
    #     super().__init__(**kwargs)
    #     self.crs = {
    #         'db': f'epsg:{conf.srid}',
    #         'geojson': f'epsg:{conf.geojson_srid}',
    #         'for_proj': f'epsg:{conf.srid_for_proj}',
    #         'survey': f'epsg:{conf.raw_survey_srid}',
    #         'web_mercator': 'epsg:3857',
    #     }

    admin: Admin
    attachments: Attachments
    basket: BasketOldDef
    # crs: Crs
    crypto: Crypto
    dashboard: Dashboard
    db: DB
    flask: Flask
    geo: Geo
    gisaf: GisafConfig
    gisaf_live: GisafLive
    jobs: list[Job]
    map: Map
    measures: Measures
    ogcapi: OGCAPI
    plot: Plot
    plugins: dict[str, dict[str, Any]]
    survey: Survey
    version: str
    weather_station: dict[str, dict[str, Any]]
    widgets: Widgets
    #engine: AsyncEngine
    #session_maker: sessionmaker

    @property
    def crs(self) -> Crs:
        return Crs(
            db=f'epsg:{self.geo.srid}',
            geojson=f'epsg:{self.geo.srid}',
            for_proj=f'epsg:{self.geo.srid_for_proj}',
            survey=f'epsg:{self.geo.raw_survey.srid}',
            web_mercator='epsg:3857',
        )

def config_file_settings() -> dict[str, Any]:
    config: dict[str, Any] = {}
    for p in config_files:
        for suffix in {".yaml", ".yml"}:
            path = p.with_suffix(suffix)
            if not path.is_file():
                logger.info(f"No file found at `{path.resolve()}`")
                continue
            logger.debug(f"Reading config file `{path.resolve()}`")
            if path.suffix in {".yaml", ".yml"}:
                config = deep_update(config, load_yaml(path))
            else:
                logger.info(f"Unknown config file extension `{path.suffix}`")
    return config


def load_yaml(path: Path) -> dict[str, Any]:
    with Path(path).open("r") as f:
        config = safe_load(f)
    if not isinstance(config, dict):
        raise TypeError(
            f"Config file has no top-level mapping: {path}"
        )
    return config


conf = Config(version=__version__)

# def set_app_config(app) -> None:
#     raw_configs = []
#     with open(Path(__file__).parent / 'defaults.yml') as cf:
#         raw_configs.append(cf.read())
#     for cf_path in (
#         Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV,
#         Path.home() / '.local' / 'gisaf' / ENV
#     ):
#         try:
#             with open(cf_path.with_suffix('.yml')) as cf:
#                 raw_configs.append(cf.read())
#         except FileNotFoundError:
#             pass

#     yaml_config = safe_load('\n'.join(raw_configs))

#     conf.app = yaml_config['app']
#     conf.postgres = yaml_config['postgres']
#     conf.storage = yaml_config['storage']
#     conf.map = yaml_config['map']
#     conf.security = yaml_config['security']
#     # create_dirs()


# def create_dirs():
#     """
#     Create the directories needed for a proper functioning of the app
#     """
#     ## Avoid circular imports
#     from treetrail.api_v1 import attachment_types
#     for type in attachment_types:
#         base_dir = Path(conf.storage['root_attachment_path']) / type
#         base_dir.mkdir(parents=True, exist_ok=True)
#     logger.info(f'Cache dir: {get_cache_dir()}')
#     get_cache_dir().mkdir(parents=True, exist_ok=True)


# def get_cache_dir() -> Path:
#     return Path(conf.storage['root_cache_path'])