import json
import logging
import logging.handlers
from pathlib import Path
from typing import Any, Dict, List, Union

try:
    import yaml
except ImportError:
    yaml = None

try:
    import aiohttp
except ImportError:
    aiohttp = None

import structlog

from jazzmine.logging.exceptions import LoggerConfigError, LoggerDependencyError, LoggerFileError
from jazzmine.logging.handlers import SyncHTTPHandler
from jazzmine.logging.workers import AsyncHttpWorker


DEFAULTS = {
    "name": "base",
    "level": "INFO",
    "json": True,
    "sinks": [{"type": "console"}],
}

SINK_REGISTRY = {
    'datadog': 'jazzmine.logging.sinks.http_sinks.DatadogLogger',
    'loki': 'jazzmine.logging.sinks.http_sinks.LokiLogger',
    'newrelic': 'jazzmine.logging.sinks.http_sinks.NewRelicLogger',
    'logstash': 'jazzmine.logging.sinks.http_sinks.LogstashLogger',
    'sentry': 'jazzmine.logging.sinks.sentry_sink.SentryLogger',
    'kafka': 'jazzmine.logging.sinks.kafka_sink.KafkaLogger',
    'redis': 'jazzmine.logging.sinks.redis_sink.RedisLogger',
    'cloudwatch': 'jazzmine.logging.sinks.cloud_sinks.CloudWatchLogger',
    'pubsub': 'jazzmine.logging.sinks.cloud_sinks.PubSubLogger',
    'vector': 'jazzmine.logging.sinks.agent_sinks.VectorLogger',
    'fluentbit': 'jazzmine.logging.sinks.agent_sinks.FluentBitLogger',
    'promtail': 'jazzmine.logging.sinks.agent_sinks.PromtailLogger',
    'graylog': 'jazzmine.logging.sinks.gelf_sink.GraylogLogger',
    'honeycomb': 'jazzmine.logging.sinks.honeycomb_sink.HoneycombLogger',
}


class ConfigLoader:
    @staticmethod
    def load(raw: Union[Dict[str, Any], str, Path]) -> Dict[str, Any]:
        if isinstance(raw, dict):
            return raw.copy()
        if isinstance(raw, (str, Path)):
            path = Path(raw)
            if not path.exists():
                raise LoggerFileError(f"Config file does not exist: {path}")
            text = path.read_text(encoding="utf-8")
            suffix = path.suffix.lower()
            
            if suffix == ".json":
                try:
                    return json.loads(text)
                except Exception as e:
                    raise LoggerConfigError(f"Invalid JSON config: {e}") from e
            
            if suffix in (".yaml", ".yml"):
                if yaml is None:
                    raise LoggerDependencyError("YAML config requested but pyyaml is not installed.")
                try:
                    return yaml.safe_load(text) or {}
                except Exception as e:
                    raise LoggerConfigError(f"Invalid YAML config: {e}") from e
            
            try:
                return json.loads(text)
            except Exception:
                if yaml is not None:
                    try:
                        return yaml.safe_load(text) or {}
                    except Exception as e:
                        raise LoggerConfigError(f"Unknown config format and parse failed: {e}") from e
                raise LoggerConfigError("Unknown config file format (must be .json/.yml/.yaml or valid json/yaml content)")
        raise LoggerConfigError("Config must be dict or path to json/yaml file")

    @staticmethod
    def merge_defaults(cfg: Dict[str, Any]) -> Dict[str, Any]:
        merged = dict(DEFAULTS)
        for k, v in cfg.items():
            merged[k] = v
        merged["sinks"] = cfg.get("sinks", merged["sinks"])
        return merged


class ConfigValidator:
    @staticmethod
    def validate(cfg: Dict[str, Any]) -> None:
        if not isinstance(cfg.get("name"), str):
            raise LoggerConfigError("Field 'name' must be a string")
        
        if not isinstance(cfg.get("level"), str):
            raise LoggerConfigError("Field 'level' must be a string like 'INFO'")
        
        if not isinstance(cfg.get("json"), bool):
            raise LoggerConfigError("Field 'json' must be boolean")
        
        sinks = cfg.get("sinks")
        if not isinstance(sinks, list) or not sinks:
            raise LoggerConfigError("Field 'sinks' must be a non-empty list")
        
        for idx, s in enumerate(sinks):
            if not isinstance(s, dict) or "type" not in s:
                raise LoggerConfigError(f"sinks[{idx}] must be a dict with a 'type' field")
            
            stype = s["type"]
            valid_types = ["console", "file", "http"] + list(SINK_REGISTRY.keys())
            if stype not in valid_types:
                raise LoggerConfigError(f"Unsupported sink type: {stype}. Valid types: {', '.join(valid_types)}")

            if stype == "file":
                if "path" not in s:
                    raise LoggerConfigError("File sink requires 'path'")
                p = Path(s["path"])
                try:
                    p.parent.mkdir(parents=True, exist_ok=True)
                    p.touch(exist_ok=True)
                except Exception as e:
                    raise LoggerFileError(f"Unable to create/touch log file {p}: {e}") from e

            if stype == "http":
                if "url" not in s:
                    raise LoggerConfigError("HTTP sink requires 'url'")
                if s.get("async", False) and aiohttp is None:
                    raise LoggerDependencyError("aiohttp required for async http sinks")


class LoggingConfigurator:
    @staticmethod
    def configure_stdlib(cfg: Dict[str, Any]) -> None:
        level_name = cfg.get("level", "INFO").upper()
        level = getattr(logging, level_name, None)
        if level is None:
            raise LoggerConfigError(f"Unknown level: {level_name}")

        root = logging.getLogger()
        for h in list(root.handlers):
            root.removeHandler(h)

        handlers: List[logging.Handler] = []
        for sink in cfg.get("sinks", []):
            stype = sink["type"]
            if stype == "console":
                ch = logging.StreamHandler()
                ch.setLevel(level)
                handlers.append(ch)
            elif stype == "file":
                path = Path(sink["path"])
                rotate = sink.get("rotate")
                if rotate:
                    when = rotate.get("when", "midnight")
                    backup = int(rotate.get("backupCount", 7))
                    fh = logging.handlers.TimedRotatingFileHandler(str(path), when=when, backupCount=backup)
                else:
                    fh = logging.FileHandler(str(path))
                fh.setLevel(level)
                handlers.append(fh)
            elif stype == "http":
                url = sink["url"]
                method = sink.get("method", "POST")
                headers = sink.get("headers")
                sync_handler = SyncHTTPHandler(url=url, method=method, headers=headers)
                sync_handler.setLevel(level)
                handlers.append(sync_handler)

        formatter = logging.Formatter("%(message)s")
        for h in handlers:
            h.setFormatter(formatter)
            root.addHandler(h)
        root.setLevel(level)

    @staticmethod
    def configure_structlog(cfg: Dict[str, Any], async_workers: Dict[str, AsyncHttpWorker]) -> None:
        processors = [
            structlog.contextvars.merge_contextvars,
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.processors.add_log_level,
            structlog.processors.StackInfoRenderer(),
            structlog.processors.format_exc_info,
        ]

        if async_workers:
            import asyncio
            
            def _async_submit_processor(logger, method_name, event_dict):
                try:
                    loop = asyncio.get_event_loop()
                except RuntimeError:
                    loop = None

                if loop and loop.is_running():
                    for w in async_workers.values():
                        asyncio.create_task(w.submit(event_dict.copy()))
                return event_dict

            processors.append(_async_submit_processor)

        if cfg.get("json", True):
            processors.append(structlog.processors.JSONRenderer())
        else:
            processors.append(structlog.dev.ConsoleRenderer())

        level_name = cfg.get("level", "INFO").upper()
        levelno = getattr(logging, level_name, logging.INFO)

        structlog.configure(
            processors=processors,
            wrapper_class=structlog.make_filtering_bound_logger(levelno),
            logger_factory=structlog.stdlib.LoggerFactory(),
            cache_logger_on_first_use=True,
        )

    @staticmethod
    def create_async_workers(cfg: Dict[str, Any]) -> Dict[str, AsyncHttpWorker]:
        workers = {}
        for idx, sink in enumerate(cfg.get("sinks", [])):
            if sink["type"] == "http" and sink.get("async", False):
                url = sink["url"]
                method = sink.get("method", "POST")
                headers = sink.get("headers", {})
                wid = f"http-async-{idx}-{url}"
                if wid not in workers:
                    workers[wid] = AsyncHttpWorker(
                        url=url,
                        method=method,
                        headers=headers,
                        max_queue=sink.get("queue_max", 1000)
                    )
        return workers

    @staticmethod
    def create_sink_loggers(cfg: Dict[str, Any]) -> List[Any]:
        """Create sink logger instances based on config"""
        sink_loggers = []
        logger_name = cfg.get("name", "base")
        level = cfg.get("level", "INFO")
        use_json = cfg.get("json", True)
        
        for sink_config in cfg.get("sinks", []):
            sink_type = sink_config["type"]
            
            # Skip console, file, http (handled by stdlib/structlog)
            if sink_type in ("console", "file", "http"):
                continue
            
            # Get sink class from registry
            if sink_type in SINK_REGISTRY:
                module_path = SINK_REGISTRY[sink_type]
                module_name, class_name = module_path.rsplit('.', 1)
                
                try:
                    import importlib
                    module = importlib.import_module(module_name)
                    sink_class = getattr(module, class_name)
                    
                    # Instantiate sink logger
                    sink_logger = sink_class(
                        sink_config=sink_config,
                        logger_name=logger_name,
                        level=level,
                        use_json=use_json
                    )
                    sink_loggers.append(sink_logger)
                except Exception as e:
                    # Log error but don't fail - allow partial sink initialization
                    import warnings
                    warnings.warn(f"Failed to initialize {sink_type} sink: {e}")
        
        return sink_loggers
