from microdot import Microdot
from microdot.session import with_session
from models.profile import Profile
from models.zone import Zone
from models.preset import Preset
import json

controller = Microdot()
profiles = Profile()
zones = Zone()
presets = Preset()

@controller.get('')
@with_session
async def list_profiles(request, session):
    """List all profiles with current profile info."""
    profile_list = profiles.list()
    current_id = session.get('current_profile')
    if current_id and current_id not in profile_list:
        current_id = None
    
    # If no current profile in session, use first one
    if not current_id and profile_list:
        current_id = profile_list[0]
        session['current_profile'] = str(current_id)
        session.save()
    
    # Build profiles object
    profiles_data = {}
    for profile_id in profile_list:
        profile_data = profiles.read(profile_id)
        if profile_data:
            profiles_data[profile_id] = profile_data
    
    return json.dumps({
        "profiles": profiles_data,
        "current_profile_id": current_id
    }), 200, {'Content-Type': 'application/json'}

@controller.get('/current')
@with_session
async def get_current_profile(request, session):
    """Get the current profile ID from session (or fallback)."""
    profile_list = profiles.list()
    current_id = session.get('current_profile')
    if current_id and current_id not in profile_list:
        current_id = None
    if not current_id and profile_list:
        current_id = profile_list[0]
        session['current_profile'] = str(current_id)
        session.save()
    if current_id:
        profile = profiles.read(current_id)
        return json.dumps({"id": current_id, "profile": profile}), 200, {'Content-Type': 'application/json'}
    return json.dumps({"error": "No profile available"}), 404

@controller.get('/<id>')
@with_session
async def get_profile(request, id, session):
    """Get a specific profile by ID."""
    # Handle 'current' as a special case
    if id == 'current':
        return await get_current_profile(request, session)
    
    profile = profiles.read(id)
    if profile:
        return json.dumps(profile), 200, {'Content-Type': 'application/json'}
    return json.dumps({"error": "Profile not found"}), 404

@controller.post('/<id>/apply')
@with_session
async def apply_profile(request, session, id):
    """Apply a profile by saving it to session."""
    if not profiles.read(id):
        return json.dumps({"error": "Profile not found"}), 404
    session['current_profile'] = str(id)
    session.save()
    return json.dumps({"message": "Profile applied", "id": str(id)}), 200, {'Content-Type': 'application/json'}

@controller.post('')
async def create_profile(request):
    """Create a new profile."""
    try:
        data = dict(request.json or {})
        name = data.get("name", "")
        seed_raw = data.get("seed_dj_zone", False)
        if isinstance(seed_raw, str):
            seed_dj_zone = seed_raw.strip().lower() in ("1", "true", "yes", "on")
        else:
            seed_dj_zone = bool(seed_raw)
        # Request-only flag: do not persist on profile records.
        data.pop("seed_dj_zone", None)
        profile_id = profiles.create(name)
        # Avoid persisting request-only fields.
        data.pop("name", None)
        if data:
            profiles.update(profile_id, data)

        # New profiles always start with a default zone pre-populated with starter presets.
        default_preset_ids = []
        default_preset_defs = [
            {
                "name": "on",
                "pattern": "on",
                "colors": ["#FFFFFF"],
                "brightness": 255,
                "delay": 100,
                "auto": True,
            },
            {
                "name": "off",
                "pattern": "off",
                "colors": [],
                "brightness": 0,
                "delay": 100,
                "auto": True,
            },
            {
                "name": "rainbow",
                "pattern": "rainbow",
                "colors": [],
                "brightness": 255,
                "delay": 100,
                "auto": True,
                "n1": 2,
            },
            {
                "name": "Colour Cycle",
                "pattern": "colour_cycle",
                "colors": ["#FF0000", "#00FF00", "#0000FF"],
                "brightness": 255,
                "delay": 100,
                "auto": True,
                "n1": 1,
            },
            {
                "name": "transition",
                "pattern": "transition",
                "colors": ["#FF0000", "#00FF00", "#0000FF"],
                "brightness": 255,
                "delay": 500,
                "auto": True,
            },
            {
                "name": "flicker",
                "pattern": "flicker",
                "colors": ["#FFB84D"],
                "brightness": 255,
                "delay": 80,
                "auto": True,
                "n1": 30,
            },
            {
                "name": "flame",
                "pattern": "flame",
                "colors": [],
                "brightness": 255,
                "delay": 50,
                "auto": True,
                "n1": 35,
                "n2": 2600,
                "n3": 0,
                "n4": 0,
            },
            {
                "name": "twinkle",
                "pattern": "twinkle",
                "colors": ["#78C8FF", "#508CFF", "#B478FF", "#64DCE8", "#A0C8FF"],
                "brightness": 255,
                "delay": 55,
                "auto": True,
                "n1": 72,
                "n2": 140,
                "n3": 2,
                "n4": 6,
            },
        ]

        for preset_data in default_preset_defs:
            pid = presets.create(profile_id)
            presets.update(pid, preset_data)
            default_preset_ids.append(str(pid))

        default_tab_id = zones.create(name="default", names=["1"], presets=[default_preset_ids])
        zones.update(default_tab_id, {
            "presets_flat": default_preset_ids,
            "default_preset": default_preset_ids[0] if default_preset_ids else None,
        })

        profile = profiles.read(profile_id) or {}
        profile_tabs = profile.get("zones", []) if isinstance(profile.get("zones", []), list) else []
        profile_tabs.append(str(default_tab_id))

        if seed_dj_zone:
            # Seed a DJ-focused zone with three starter presets.
            seeded_preset_ids = []
            preset_defs = [
                {
                    "name": "DJ Rainbow",
                    "pattern": "rainbow",
                    "colors": [],
                    "brightness": 220,
                    "delay": 60,
                    "n1": 12,
                },
                {
                    "name": "DJ Single Color",
                    "pattern": "on",
                    "colors": ["#ff00ff"],
                    "brightness": 220,
                    "delay": 100,
                },
                {
                    "name": "DJ Transition",
                    "pattern": "transition",
                    "colors": ["#ff0000", "#00ff00", "#0000ff"],
                    "brightness": 220,
                    "delay": 250,
                },
            ]

            for preset_data in preset_defs:
                pid = presets.create(profile_id)
                presets.update(pid, preset_data)
                seeded_preset_ids.append(str(pid))

            dj_tab_id = zones.create(name="dj", names=["dj"], presets=[seeded_preset_ids])
            zones.update(dj_tab_id, {
                "presets_flat": seeded_preset_ids,
                "default_preset": seeded_preset_ids[0] if seeded_preset_ids else None,
            })

            profile_tabs.append(str(dj_tab_id))

        profiles.update(profile_id, {"zones": profile_tabs})

        profile_data = profiles.read(profile_id)
        return json.dumps({profile_id: profile_data}), 201, {'Content-Type': 'application/json'}
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.post('/<id>/clone')
async def clone_profile(request, id):
    """Clone an existing profile along with its tabs and palette."""
    try:
        source = profiles.read(id)
        if not source:
            return json.dumps({"error": "Profile not found"}), 404

        data = request.json or {}
        source_name = source.get("name") or f"Profile {id}"
        new_name = data.get("name") or source_name
        profile_type = source.get("type", "zones")

        def allocate_id(model, cache):
            if "next" not in cache:
                max_id = max((int(k) for k in model.keys() if str(k).isdigit()), default=0)
                cache["next"] = max_id + 1
            next_id = str(cache["next"])
            cache["next"] += 1
            return next_id

        def map_preset_container(value, id_map, preset_cache, new_profile_id, new_presets):
            if isinstance(value, list):
                return [map_preset_container(v, id_map, preset_cache, new_profile_id, new_presets) for v in value]
            if value is None:
                return None
            preset_id = str(value)
            if preset_id in id_map:
                return id_map[preset_id]
            preset_data = presets.read(preset_id)
            if not preset_data:
                return None
            new_preset_id = allocate_id(presets, preset_cache)
            clone_data = dict(preset_data)
            clone_data["profile_id"] = str(new_profile_id)
            new_presets[new_preset_id] = clone_data
            id_map[preset_id] = new_preset_id
            return new_preset_id

        # Prepare new IDs without writing until everything is ready.
        profile_cache = {}
        palette_cache = {}
        tab_cache = {}
        preset_cache = {}

        new_profile_id = allocate_id(profiles, profile_cache)
        new_palette_id = allocate_id(profiles._palette_model, palette_cache)

        # Clone palette colors into the new profile's palette
        src_palette_id = source.get("palette_id")
        palette_colors = []
        if src_palette_id:
            try:
                palette_colors = profiles._palette_model.read(src_palette_id)
            except Exception:
                palette_colors = []

        # Clone tabs and presets used by those tabs
        source_tabs = source.get("zones")
        if not isinstance(source_tabs, list) or len(source_tabs) == 0:
            source_tabs = source.get("zone_order", [])
        source_tabs = source_tabs or []
        cloned_tab_ids = []
        preset_id_map = {}
        new_tabs = {}
        new_presets = {}
        for zone_id in source_tabs:
            zone = zones.read(zone_id)
            if not zone:
                continue
            tab_name = zone.get("name") or f"Zone {zone_id}"
            clone_name = tab_name
            mapped_presets = map_preset_container(zone.get("presets"), preset_id_map, preset_cache, new_profile_id, new_presets)
            clone_id = allocate_id(zones, tab_cache)
            clone_data = {
                "name": clone_name,
                "names": zone.get("names") or [],
                "presets": mapped_presets if mapped_presets is not None else []
            }
            extra = {k: v for k, v in zone.items() if k not in ("name", "names", "presets")}
            if "presets_flat" in extra:
                extra["presets_flat"] = map_preset_container(extra.get("presets_flat"), preset_id_map, preset_cache, new_profile_id, new_presets)
            if extra:
                clone_data.update(extra)
            new_tabs[clone_id] = clone_data
            cloned_tab_ids.append(clone_id)

        new_profile_data = {
            "name": new_name,
            "type": profile_type,
            "zones": cloned_tab_ids,
            "scenes": list(source.get("scenes", [])) if isinstance(source.get("scenes", []), list) else [],
            "palette_id": str(new_palette_id),
        }

        # Commit all changes and save once per model.
        profiles._palette_model[str(new_palette_id)] = list(palette_colors) if palette_colors else []
        for pid, pdata in new_presets.items():
            presets[pid] = pdata
        for tid, tdata in new_tabs.items():
            zones[tid] = tdata
        profiles[str(new_profile_id)] = new_profile_data

        profiles._palette_model.save()
        presets.save()
        zones.save()
        profiles.save()

        return json.dumps({new_profile_id: new_profile_data}), 201, {'Content-Type': 'application/json'}
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.put('/current')
@with_session
async def update_current_profile(request, session):
    """Update the current profile using session (or fallback)."""
    try:
        data = request.json or {}
        profile_list = profiles.list()
        current_id = session.get('current_profile')
        if not current_id and profile_list:
            current_id = profile_list[0]
            session['current_profile'] = str(current_id)
            session.save()
        if not current_id:
            return json.dumps({"error": "No profile available"}), 404
        if profiles.update(current_id, data):
            return json.dumps(profiles.read(current_id)), 200, {'Content-Type': 'application/json'}
        return json.dumps({"error": "Profile not found"}), 404
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.put('/<id>')
async def update_profile(request, id):
    """Update an existing profile."""
    try:
        data = request.json
        if profiles.update(id, data):
            return json.dumps(profiles.read(id)), 200, {'Content-Type': 'application/json'}
        return json.dumps({"error": "Profile not found"}), 404
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.delete('/<id>')
async def delete_profile(request, id):
    """Delete a profile."""
    if profiles.delete(id):
        return json.dumps({"message": "Profile deleted successfully"}), 200
    return json.dumps({"error": "Profile not found"}), 404
