

import traceback
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
class Chating(AsyncWebsocketConsumer):

    # class-level, in-memory viewer counter: { group_name: count }
    viewer_counts = {}

    # ── connect ────────────────────────────────
    async def connect(self):
        self.match_id = self.scope["url_route"]["kwargs"]["match_id"]
        self.group_name = f"live_match_{self.match_id}"

        await self.channel_layer.group_add(self.group_name, self.channel_name)
        await self.accept()

        # Bump viewer count for this match and tell everyone (scorer + viewers)
        Chating.viewer_counts[self.group_name] = Chating.viewer_counts.get(self.group_name, 0) + 1
        await self.notify_viewer_count()

        state = await self.get_match_state()
        await self.send(text_data=json.dumps({
            "type": "init",
            "payload": state,
        }))

    # ── disconnect ─────────────────────────────
    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.group_name, self.channel_name)

        current = Chating.viewer_counts.get(self.group_name, 1)
        Chating.viewer_counts[self.group_name] = max(0, current - 1)
        await self.notify_viewer_count()

    # ── receive from client ────────────────────
    async def receive(self, text_data):
        try:
            data = json.loads(text_data)
            msg_type = data.get("type")

            if msg_type == "score_update":
                updated = await self.update_score(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_score", "payload": updated}
                )

            elif msg_type == "wicket":
                updated = await self.handle_wicket(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_score", "payload": updated}
                )

            elif msg_type == "swap_strike":
                updated = await self.swap_strike(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_score", "payload": updated}
                )

            elif msg_type == "set_bowler":
                updated = await self.set_bowler(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_score", "payload": updated}
                )

            elif msg_type == "set_batsmen":
                updated = await self.set_batsmen(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_score", "payload": updated}
                )

            elif msg_type == "end_innings":
                updated = await self.end_innings(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_innings_end", "payload": updated}
                )

            elif msg_type == "end_match":
                updated = await self.end_match(data["payload"])
                await self.channel_layer.group_send(
                    self.group_name, {"type": "broadcast_match_end", "payload": updated}
                )

            elif msg_type == "ping":
                await self.send(text_data=json.dumps({"type": "pong"}))

        except Exception as e:
            traceback.print_exc()
            await self.send(text_data=json.dumps({
                "type": "error",
                "message": str(e),
            }))

    # ── broadcast handlers (called by group_send) ──
    async def broadcast_score(self, event):
        await self.send(text_data=json.dumps({
            "type": "score_update",
            "payload": event["payload"],
        }))

    async def broadcast_innings_end(self, event):
        await self.send(text_data=json.dumps({
            "type": "innings_end",
            "payload": event["payload"],
        }))

    async def broadcast_match_end(self, event):
        await self.send(text_data=json.dumps({
            "type": "match_end",
            "payload": event["payload"],
        }))

    async def viewer_count_message(self, event):
        """Handler for the 'viewer_count' group_send — pushed to every
        connected socket (scorer's StreamBadge + every viewer's LiveBadge)."""
        await self.send(text_data=json.dumps({
            "type": "viewer_count",
            "payload": event["payload"],
        }))

    # ── helper: compute + broadcast current viewer count ──
    async def notify_viewer_count(self):
        count = Chating.viewer_counts.get(self.group_name, 0)
        await self.channel_layer.group_send(
            self.group_name,
            {"type": "viewer_count_message", "payload": {"viewer_count": count}},
        )

    # ══════════════════════════════════════════
    #  DB OPERATIONS  (sync → async)
    # ══════════════════════════════════════════

    @database_sync_to_async
    def get_match_state(self):
        """Full state for initial load. Wrapped so any DB error is visible instead of silently blanking the screen."""
        from .models import Match, LiveScore
        try:
            match = Match.objects.select_related(
                "team_a", "team_b",
                "batting_team", "bowling_team",
                "striker", "non_striker", "bowler",
                "toss_winner",
            ).get(id=self.match_id)

            live, _ = LiveScore.objects.get_or_create(match=match)
            return self._full_state(match, live)
        except Exception as e:
            traceback.print_exc()
            return {"error": str(e)}

    def _full_state(self, match, live):
        """
        Single source of truth for state — used on connect AND after every
        action, so the frontend never has stale batsmen_scores/bowler_figures.
        """

        def player_list(team):
            if not team:
                return []
            return list(team.players.values("id", "name", "role", "player_id"))

        team_a_players = player_list(match.team_a)
        team_b_players = player_list(match.team_b)
        batting_players = player_list(match.batting_team)
        bowling_players = player_list(match.bowling_team)

        batsmen_scores = {
            str(bs.player_id): {
                "runs": bs.runs, "balls": bs.balls,
                "fours": bs.fours, "sixes": bs.sixes,
                "is_out": bs.is_out, "dismissal": bs.dismissal,
                "innings": bs.innings,
            }
            for bs in live.batsmanscores.select_related("player").all()
        }

        bowler_figures = {
            str(bf.player_id): {
                "overs": bf.overs, "maidens": bf.maidens,
                "runs": bf.runs, "wickets": bf.wickets,
                "innings": bf.innings,
            }
            for bf in live.bowlerfigures.select_related("player").all()
        }

        return {
            "match_id": match.id,
            "status": match.status,
            "total_overs": match.overs,
            "venue": match.venue,
            "match_date": str(match.match_date) if match.match_date else "",

            # Teams
            "team_a": {
                "id": match.team_a.id, "name": match.team_a.name,
                "emoji": match.team_a.logo_emoji, "color": match.team_a.logo_color,
            },
            "team_b": {
                "id": match.team_b.id, "name": match.team_b.name,
                "emoji": match.team_b.logo_emoji, "color": match.team_b.logo_color,
            },
            "batting_team_id": match.batting_team.id if match.batting_team else None,
            "bowling_team_id": match.bowling_team.id if match.bowling_team else None,

            # Toss info
            "toss_winner_id": match.toss_winner.id if match.toss_winner else None,
            "toss_winner_name": match.toss_winner.name if match.toss_winner else "",
            "toss_choice": match.toss_choice or "",

            # Full squads — needed so scorecard can show BOTH teams' players
            "team_a_players": team_a_players,
            "team_b_players": team_b_players,

            # Current-innings squads (who's batting / bowling right now)
            "batting_players": batting_players,
            "bowling_players": bowling_players,

            # Current players on field
            "striker_id": match.striker.id if match.striker else None,
            "striker_name": match.striker.name if match.striker else "",
            "non_striker_id": match.non_striker.id if match.non_striker else None,
            "non_striker_name": match.non_striker.name if match.non_striker else "",
            "bowler_id": match.bowler.id if match.bowler else None,
            "bowler_name": match.bowler.name if match.bowler else "",

            # Live score
            "innings": live.innings,
            "runs": live.runs,
            "wickets": live.wickets,
            "balls": live.balls,
            "extras": live.extras,
            "this_over": live.this_over_json(),
            "target": live.target,

            # Innings 1 saved totals (used in innings 2 for target display)
            "innings1_runs": live.innings1_runs,
            "innings1_wickets": live.innings1_wickets,
            "innings1_balls": live.innings1_balls,

            # Per-player stats — keyed by player id, tagged with which innings
            "batsmen_scores": batsmen_scores,
            "bowler_figures": bowler_figures,
        }

    @database_sync_to_async
    def update_score(self, payload):
        """
        payload = {
            runs: int, is_extra: bool, extra_type: "wide"|"no_ball"|"leg_bye"|"bye",
            striker_id: int, bowler_id: int, match_id: int,
        }
        """
        from .models import Match, LiveScore, BatsmanScore, BowlerFigure

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        runs = int(payload.get("runs", 0))
        is_extra = payload.get("is_extra", False)
        extra_type = payload.get("extra_type", "")
        striker_id = payload.get("striker_id")
        bowler_id = payload.get("bowler_id")

        live.runs += runs
        live.extras += runs if is_extra else 0

        # Wide & no-ball don't count as a legal delivery
        if extra_type not in ("wide", "no_ball"):
            live.balls += 1

        # Batsman personal runs: NOT credited on wide, bye, or leg-bye
        # (those go to the team total / extras only — genuine cricket rule).
        # No-ball penalty run itself also isn't a "batsman run", only actual
        # runs scored off the bat on a no-ball would be — this simplified UI
        # sends the penalty separately, so we don't credit the striker here.
        if striker_id and extra_type not in ("wide", "bye", "leg_bye"):
            bs, _ = BatsmanScore.objects.get_or_create(
                live=live, player_id=striker_id, innings=live.innings
            )
            bs.runs += runs
            bs.balls += 1 if extra_type not in ("wide", "no_ball") else 0
            bs.fours += 1 if runs == 4 else 0
            bs.sixes += 1 if runs == 6 else 0
            bs.save()
        elif striker_id and extra_type in ("bye", "leg_bye"):
            # Batsman still "faces" the ball for strike-rotation purposes,
            # but no runs/boundary credit — just track balls faced.
            bs, _ = BatsmanScore.objects.get_or_create(
                live=live, player_id=striker_id, innings=live.innings
            )
            bs.balls += 1
            bs.save()

        # Bowler figures (byes/leg-byes don't count against the bowler's runs conceded in some rule sets,
        # but most club-level scoring still adds them to the bowler's runs column — kept as before)
        if bowler_id:
            bf, _ = BowlerFigure.objects.get_or_create(
                live=live, player_id=bowler_id, innings=live.innings
            )
            bf.runs += runs
            if extra_type not in ("wide", "no_ball"):
                bf.balls += 1
                bf.overs = f"{bf.balls // 6}.{bf.balls % 6}"
            bf.save()

        # This-over ball display
        if is_extra:
            over_ball = {"wide": "WD", "no_ball": "NB", "leg_bye": "LB", "bye": "B"}.get(extra_type, "Ex")
        else:
            over_ball = "•" if runs == 0 else str(runs)
        live.add_to_this_over(over_ball)
        live.save()

        # Rotate strike on odd runs (off bat, or byes/leg-byes since batsmen physically run)
        if runs % 2 == 1 and extra_type != "wide":
            match.striker, match.non_striker = match.non_striker, match.striker
            match.save(update_fields=["striker", "non_striker"])

        return self._full_state(match, live)

    @database_sync_to_async
    def handle_wicket(self, payload):
        from .models import Match, LiveScore, BatsmanScore, BowlerFigure, Player

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        striker_id = payload.get("striker_id")
        bowler_id = payload.get("bowler_id")
        dismissal = payload.get("dismissal", "Out")
        new_batsman_id = payload.get("new_batsman_id")

        live.wickets += 1
        live.balls += 1
        live.add_to_this_over("W")
        live.save()

        if striker_id:
            bs, _ = BatsmanScore.objects.get_or_create(
                live=live, player_id=striker_id, innings=live.innings
            )
            bs.is_out = True
            bs.dismissal = dismissal
            bs.balls += 1
            bs.save()

        if bowler_id:
            bf, _ = BowlerFigure.objects.get_or_create(
                live=live, player_id=bowler_id, innings=live.innings
            )
            bf.wickets += 1
            bf.balls += 1
            bf.overs = f"{bf.balls // 6}.{bf.balls % 6}"
            bf.save()

        if new_batsman_id:
            match.striker = Player.objects.get(id=new_batsman_id)
            match.save(update_fields=["striker"])

        return self._full_state(match, live)

    @database_sync_to_async
    def swap_strike(self, payload):
        """Manually swap striker <-> non-striker (button press, not tied to runs)."""
        from .models import Match, LiveScore

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        match.striker, match.non_striker = match.non_striker, match.striker
        match.save(update_fields=["striker", "non_striker"])

        return self._full_state(match, live)

    @database_sync_to_async
    def set_bowler(self, payload):
        """Set a new bowler (called from the 'New Over / Bowler' picker)."""
        from .models import Match, LiveScore, Player

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        bowler_id = payload.get("bowler_id")
        if bowler_id:
            match.bowler = Player.objects.get(id=bowler_id)
            match.save(update_fields=["bowler"])

        return self._full_state(match, live)

    @database_sync_to_async
    def set_batsmen(self, payload):
        """Set opening (or replacement) striker + non-striker together — used at the
        start of every innings since end_innings() clears both to None."""
        from .models import Match, LiveScore, Player

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        striker_id = payload.get("striker_id")
        non_striker_id = payload.get("non_striker_id")

        if striker_id:
            match.striker = Player.objects.get(id=striker_id)
        if non_striker_id:
            match.non_striker = Player.objects.get(id=non_striker_id)
        match.save(update_fields=["striker", "non_striker"])

        return self._full_state(match, live)

    @database_sync_to_async
    def end_innings(self, payload):
        from .models import Match, LiveScore

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)

        live.innings1_runs = live.runs
        live.innings1_wickets = live.wickets
        live.innings1_balls = live.balls
        live.target = live.runs + 1
        live.innings = 2
        live.runs = 0
        live.wickets = 0
        live.balls = 0
        live.extras = 0
        live.this_over_list = "[]"
        live.save()

        # Swap batting/bowling teams for innings 2
        match.batting_team, match.bowling_team = match.bowling_team, match.batting_team
        # Clear on-field players — frontend MUST prompt "set_batsmen" + "set_bowler"
        # before the 2nd innings can start, otherwise names show blank.
        match.striker = None
        match.non_striker = None
        match.bowler = None
        match.save(update_fields=["batting_team", "bowling_team", "striker", "non_striker", "bowler"])

        return self._full_state(match, live)

    @database_sync_to_async
    def end_match(self, payload):
        from .models import Match, LiveScore

        match = Match.objects.select_related(
            "team_a", "team_b", "batting_team", "bowling_team",
            "striker", "non_striker", "bowler",
        ).get(id=self.match_id)
        live, _ = LiveScore.objects.get_or_create(match=match)
        winner = payload.get("winner", "")

        match.status = "completed"
        match.save(update_fields=["status"])

        state = self._full_state(match, live)
        state["winner"] = winner
        return state

class WebRTCConsumer(AsyncWebsocketConsumer):
    

    async def connect(self):
        self.room_id    = self.scope["url_route"]["kwargs"]["room_id"]
        self.group_name = f"webrtc_room_{self.room_id}"
        self.role       = None  # set on 'join' message

        await self.channel_layer.group_add(self.group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        # Notify viewers broadcaster left
        if self.role == "broadcaster":
            await self.channel_layer.group_send(self.group_name, {
                "type":    "relay_message",
                "payload": { "type": "broadcaster_left" },
                "sender":  self.channel_name,
            })
        await self.channel_layer.group_discard(self.group_name, self.channel_name)

    async def receive(self, text_data):
        try:
            data     = json.loads(text_data)
            msg_type = data.get("type")

            if msg_type == "join":
                self.role = data.get("role", "viewer")
                # Confirm join
                await self.send(text_data=json.dumps({
                    "type": "joined",
                    "role": self.role,
                    "room": self.room_id,
                }))
                # Tell broadcaster a new viewer joined
                if self.role == "viewer":
                    await self.channel_layer.group_send(self.group_name, {
                        "type":    "relay_message",
                        "payload": { "type": "viewer_joined", "viewer_channel": self.channel_name },
                        "sender":  self.channel_name,
                    })

            elif msg_type == "offer":
                # Broadcaster → relay offer to all viewers
                await self.channel_layer.group_send(self.group_name, {
                    "type":    "relay_message",
                    "payload": { "type": "offer", "sdp": data.get("sdp") },
                    "sender":  self.channel_name,
                })

            elif msg_type == "answer":
                # Viewer → relay answer to broadcaster
                await self.channel_layer.group_send(self.group_name, {
                    "type":    "relay_message",
                    "payload": { "type": "answer", "sdp": data.get("sdp") },
                    "sender":  self.channel_name,
                })

            elif msg_type == "candidate":
                # Relay ICE candidate to all others
                await self.channel_layer.group_send(self.group_name, {
                    "type":    "relay_message",
                    "payload": {
                        "type":      "candidate",
                        "candidate": data.get("candidate"),
                    },
                    "sender": self.channel_name,
                })

            elif msg_type == "ping":
                await self.send(text_data=json.dumps({"type": "pong"}))

        except Exception as e:
            await self.send(text_data=json.dumps({ "type": "error", "message": str(e) }))

    # Called by group_send
    async def relay_message(self, event):
        # Don't send back to sender
        if event.get("sender") == self.channel_name:
            return
        await self.send(text_data=json.dumps(event["payload"]))