from __future__ import annotations

import os
import random
import shutil
import sys
import time
from dataclasses import dataclass

FISH_TYPES = ("🐟", "🐠", "🐡", "🦈")
SURFACE_CHAR = "~"
WATER_CHAR = " "
LINE_CHAR = "|"
HOOK_CHAR = "🪝"
FRAME_DELAY = 0.1
SPAWN_INTERVAL_RANGE = (0.7, 1.8)
HEADER_LINES = 3
HORIZONTAL_MARGIN = 4
VERTICAL_MARGIN = 1


@dataclass
class Fish:
    symbol: str
    row: int
    col: int
    direction: int


def enable_ansi_escape_sequences() -> None:
    if os.name != "nt":
        return

    try:
        import ctypes

        kernel32 = ctypes.windll.kernel32
        handle = kernel32.GetStdHandle(-11)
        mode = ctypes.c_uint32()
        if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
            kernel32.SetConsoleMode(handle, mode.value | 0x0004)
    except Exception:
        pass


class InputReader:
    def __init__(self) -> None:
        self._posix_old_settings = None
        if os.name != "nt":
            self._setup_posix()

    def _setup_posix(self) -> None:
        try:
            import termios
            import tty

            fd = sys.stdin.fileno()
            self._posix_old_settings = termios.tcgetattr(fd)
            tty.setcbreak(fd)
        except Exception:
            self._posix_old_settings = None

    def close(self) -> None:
        if os.name != "nt" and self._posix_old_settings is not None:
            try:
                import termios

                termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._posix_old_settings)
            except Exception:
                pass

    def get_key(self) -> str | None:
        if os.name == "nt":
            return self._get_key_windows()
        return self._get_key_posix()

    def _get_key_windows(self) -> str | None:
        import msvcrt

        if not msvcrt.kbhit():
            return None

        key = msvcrt.getwch()
        if key in ("\x00", "\xe0"):
            special = msvcrt.getwch()
            return {
                "H": "UP",
                "P": "DOWN",
                "K": "LEFT",
                "M": "RIGHT",
            }.get(special)
        if key in ("\r", "\n"):
            return "ENTER"
        if key == "\x03":
            raise KeyboardInterrupt
        return key.upper()

    def _get_key_posix(self) -> str | None:
        try:
            import select

            ready, _, _ = select.select([sys.stdin], [], [], 0)
            if not ready:
                return None

            key = sys.stdin.read(1)
            if key == "\x1b":
                sequence = key
                for _ in range(2):
                    ready, _, _ = select.select([sys.stdin], [], [], 0)
                    if not ready:
                        break
                    sequence += sys.stdin.read(1)
                return {
                    "\x1b[A": "UP",
                    "\x1b[B": "DOWN",
                    "\x1b[D": "LEFT",
                    "\x1b[C": "RIGHT",
                }.get(sequence)
            if key == "\x03":
                raise KeyboardInterrupt
            if key in ("\r", "\n"):
                return "ENTER"
            return key.upper()
        except Exception:
            return None


class FishingGame:
    def __init__(self) -> None:
        self.random = random.Random()
        self.score = {fish: 0 for fish in FISH_TYPES}
        self.fishes: list[Fish] = []
        self.board_width = 0
        self.screen_height = 0
        self.water_height = 0
        self.hook_x = 0
        self.hook_y = 0
        self.next_spawn_at = time.monotonic()
        self.running = True
        self.message = "Catch fish with the hook. Press Q to quit."
        self.resize()

    def resize(self) -> None:
        size = shutil.get_terminal_size((80, 26))
        self.board_width = max(26, size.columns - HORIZONTAL_MARGIN)
        self.screen_height = max(16, size.lines - VERTICAL_MARGIN)
        self.water_height = max(8, self.screen_height - HEADER_LINES - 2)
        self.hook_x = min(max(2, self.hook_x or self.board_width // 2), self.board_width - 3)
        self.hook_y = min(max(1, self.hook_y or self.water_height // 2), self.water_height - 1)
        self.fishes = [fish for fish in self.fishes if 1 <= fish.row <= self.water_height - 1]

    def handle_key(self, key: str | None) -> None:
        if key is None:
            return
        if key == "LEFT":
            self.hook_x = max(1, self.hook_x - 1)
        elif key == "RIGHT":
            self.hook_x = min(self.board_width - 2, self.hook_x + 1)
        elif key == "UP":
            self.hook_y = max(1, self.hook_y - 1)
        elif key == "DOWN":
            self.hook_y = min(self.water_height - 1, self.hook_y + 1)
        elif key == "Q":
            self.running = False

    def maybe_spawn_fish(self, now: float) -> None:
        if now < self.next_spawn_at:
            return

        symbol = self.random.choice(FISH_TYPES)
        row = self.random.randint(1, self.water_height - 1)
        direction = self.random.choice((-1, 1))
        col = 1 if direction == 1 else self.board_width - 2
        self.fishes.append(Fish(symbol=symbol, row=row, col=col, direction=direction))
        self.next_spawn_at = now + self.random.uniform(*SPAWN_INTERVAL_RANGE)

    def move_fish(self) -> None:
        survivors: list[Fish] = []
        for fish in self.fishes:
            fish.col += fish.direction
            if fish.col <= 0 or fish.col >= self.board_width - 1:
                continue
            if fish.row == self.hook_y and fish.col == self.hook_x:
                self.score[fish.symbol] += 1
                self.message = f"Caught {fish.symbol}! Keep fishing."
                continue
            survivors.append(fish)
        self.fishes = survivors

    def render(self) -> str:
        lines: list[str] = []
        score_text = "  ".join(f"{fish} {self.score[fish]}" for fish in FISH_TYPES)
        lines.append(f"fih   {score_text}")
        lines.append("Arrow keys move the line. Q quits.")
        lines.append(self.message[: self.board_width])

        for water_row in range(self.water_height + 1):
            cells = [WATER_CHAR] * self.board_width
            if water_row == 0:
                cells = [SURFACE_CHAR] * self.board_width
            if 0 <= water_row <= self.hook_y:
                cells[self.hook_x] = LINE_CHAR
            if water_row == self.hook_y:
                cells[self.hook_x] = HOOK_CHAR
            for fish in self.fishes:
                if fish.row == water_row and 0 <= fish.col < self.board_width:
                    cells[fish.col] = fish.symbol
            lines.append("".join(cells))

        return "\x1b[H\x1b[J" + "\n".join(lines)

    def run(self) -> int:
        reader = InputReader()
        enable_ansi_escape_sequences()
        sys.stdout.write("\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J")
        sys.stdout.flush()
        last_size_check = 0.0

        try:
            while self.running:
                now = time.monotonic()
                if now - last_size_check > 0.5:
                    self.resize()
                    last_size_check = now

                self.handle_key(reader.get_key())
                self.maybe_spawn_fish(now)
                self.move_fish()
                sys.stdout.write(self.render())
                sys.stdout.flush()
                time.sleep(FRAME_DELAY)
        except KeyboardInterrupt:
            return 130
        finally:
            reader.close()
            sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
            sys.stdout.flush()

        return 0


def main() -> int:
    game = FishingGame()
    return game.run()
