"""
メイン画面を定義したクラス

    「ロト6 当せん数字予測アプリ」
    Copyright (c) 2025 toshifumi tsutsui
    Released under the MIT license
    https://wpandora8.net/the_mit_license.html
"""

from threading import Thread
from typing import Any, override

import polars as pl
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.screenmanager import Screen
from kivy.uix.textinput import TextInput

import constants
from controllers.fetch_data_from_web import fetch_data_from_web
from datagrid import DataGrid
from my_dialogs import SaveFileDialog, YesNoBox
from shared_vars import SharedVars
from views.datagrid_main_row import DataGridMainHeader, DataGridMainRow

Builder.load_string("""
<MainScreen>:
    AnchorLayout:
        padding: dp(40), dp(20), dp(40), dp(10)
        BoxLayout:
            orientation: 'vertical'
            BoxLayout:
                orientation: 'horizontal'
                height: dp(48)
                size_hint_y: None
                size_hint_x: 0.2
                padding: dp(0), dp(8)
                spacing: dp(4)
                Label:
                    size_hint_x: 0.15
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '第'
                TextInput:
                    id: text_times
                    size_hint_x: 0.7
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.15
                    text_size: self.size
                    halign: 'left'
                    valign: 'middle'
                    text: '回'

            BoxLayout:
                orientation: 'horizontal'
                height: dp(48)
                size_hint_y: None
                padding: dp(0), dp(8)
                spacing: dp(2)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '1:'
                TextInput:
                    id: text_1st_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '2:'
                TextInput:
                    id: text_2nd_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '3:'
                TextInput:
                    id: text_3rd_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '4:'
                TextInput:
                    id: text_4th_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '5:'
                TextInput:
                    id: text_5th_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: '6:'
                TextInput:
                    id: text_6th_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.035
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: 'B:'
                TextInput:
                    id: text_bonus_num
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)
                Label:
                    size_hint_x: 0.115
                    text_size: self.size
                    halign: 'right'
                    valign: 'middle'
                    text: 'セット球:'
                TextInput:
                    id: text_ball_set
                    size_hint_x: 0.08
                    text_size: self.size
                    multiline: False
                    write_tab: False
                    use_bubble: True
                    on_text_validate: root.on_text_validate(self)

            AnchorLayout:
                anchor_x: 'center'
                anchor_y: 'center'
                height: dp(80)
                size_hint_y: None
                BoxLayout:
                    orientation: 'horizontal'
                    size_hint_x: 0.7
                    padding: dp(0), dp(20), dp(0), dp(20)
                    spacing: dp(16)
                    Button:
                        id: button_fetch
                        size_hint_x: 0.4
                        text: 'ウェブからデータ取得'
                        on_release: root.on_fetch_button_click()
                    Button:
                        id: button_modify
                        size_hint_x: 0.3
                        text: '追加・更新'
                        on_release: root.on_modify_button_click()
                    Button:
                        id: button_delete
                        size_hint_x: 0.3
                        text: '削除'
                        disabled: True
                        on_press: root.on_delete_button_click()

            DataGrid:
                id: datagrid
                viewclass: 'DataGridMainRow'
                size_hint: 1.0, 0.9
                pos_hint: {'center_x': 0.5, 'center_y': 0.5}

            Label:
                height: dp(60)
                size_hint_y: None
                id: label_status
                text_size: self.size
                halign: 'center'
                valign: 'middle'
                text: 'メニューを選択してください。'
""")


class MainScreen(Screen):
    """メイン画面を定義したクラス"""

    def __init__(self, shared_vars: SharedVars, **kwargs):
        """メイン画面を定義したクラスのインスタンスを作成して返す。

        Args:
            shared_vars (SharedVars): アプリ内で共有する変数を管理するクラスのインスタンス。
        """

        super().__init__(**kwargs)

        self._shared_vars = shared_vars
        """アプリ内で共有する変数を管理するクラスのインスタンス"""

        self._selected_times: int = -1
        """データグリッドの選択された行の抽選回"""

        self._setup_widget_references()

        self._datagrid.header_item = DataGridMainHeader()
        self._datagrid.bind(selected_row=self._on_select_change)  # type: ignore

        self._shared_vars.df = self._read_parquet()

    def _setup_widget_references(self) -> None:
        """各ウィジェットの参照を設定する。"""

        self._label_status: Label = self.ids.label_status
        """状況を表示するラベル"""

        self._text_times: TextInput = self.ids.text_times
        """抽選回を入力する TextInput"""

        self._text_1st_num: TextInput = self.ids.text_1st_num
        """第 1 数字を入力する TextInput"""

        self._text_2nd_num: TextInput = self.ids.text_2nd_num
        """第 2 数字を入力する TextInput"""

        self._text_3rd_num: TextInput = self.ids.text_3rd_num
        """第 3 数字を入力する TextInput"""

        self._text_4th_num: TextInput = self.ids.text_4th_num
        """第 4 数字を入力する TextInput"""

        self._text_5th_num: TextInput = self.ids.text_5th_num
        """第 5 数字を入力する TextInput"""

        self._text_6th_num: TextInput = self.ids.text_6th_num
        """第 6 数字を入力する TextInput"""

        self._text_bonus_num: TextInput = self.ids.text_bonus_num
        """ボーナス数字を入力する TextInput"""

        self._text_ball_set: TextInput = self.ids.text_ball_set
        """セット球を入力する TextInput"""

        self._button_delete: Button = self.ids.button_delete
        """削除ボタン"""

        self._datagrid: DataGrid = self.ids.datagrid
        """データグリッドのインスタンス"""

    @override
    def on_pre_enter(self, *args) -> None:
        """画面が表示されるとき。"""

        self._display_data()
        self._text_times.focus = True
        self._text_times.select_all()

    def _on_select_change(self, _, item: DataGridMainRow) -> None:
        """データグリッドの選択が変更されたとき。

        Args:
            item (RecordItem): 選択されたアイテム。
        """

        if item.is_dummy:
            self._selected_index = -1
            self._clear_input_text()
            self._display_status("")
            self._button_delete.disabled = True
        else:
            self._selected_times = item.times
            self._text_times.text = str(item.times)
            self._text_ball_set.text = item.ball_set
            self._text_1st_num.text = f"{item.first_num:0>2}"
            self._text_2nd_num.text = f"{item.second_num:0>2}"
            self._text_3rd_num.text = f"{item.third_num:0>2}"
            self._text_4th_num.text = f"{item.fourth_num:0>2}"
            self._text_5th_num.text = f"{item.fifth_num:0>2}"
            self._text_6th_num.text = f"{item.sixth_num:0>2}"
            self._text_bonus_num.text = f"{item.bonus_num:0>2}"
            self._button_delete.disabled = False

            self._display_status(f"'第 {item.times} 回' のデータが選択されました。")

    def on_text_validate(self, text_input: TextInput) -> None:
        """テキストボックスの入力が確定したとき。

        Args:
            text_input (TextInput): 入力されたテキストボックス。
        """

        if text_input is self._text_times:
            self._text_1st_num.focus = True
            self._text_1st_num.select_all()
        elif text_input is self._text_1st_num:
            self._text_2nd_num.focus = True
            self._text_2nd_num.select_all()
        elif text_input is self._text_2nd_num:
            self._text_3rd_num.focus = True
            self._text_3rd_num.select_all()
        elif text_input is self._text_3rd_num:
            self._text_4th_num.focus = True
            self._text_4th_num.select_all()
        elif text_input is self._text_4th_num:
            self._text_5th_num.focus = True
            self._text_5th_num.select_all()
        elif text_input is self._text_5th_num:
            self._text_6th_num.focus = True
            self._text_6th_num.select_all()
        elif text_input is self._text_6th_num:
            self._text_bonus_num.focus = True
            self._text_bonus_num.select_all()
        elif text_input is self._text_bonus_num:
            self._text_ball_set.focus = True
            self._text_ball_set.select_all()
        elif text_input is self._text_ball_set:
            self._text_ball_set.text = self._text_ball_set.text.upper()
            self.on_modify_button_click()

    def on_fetch_button_click(self) -> None:
        """「データを取得」ボタンがクリックされたとき。"""

        self._display_status("ウェブサイトからデータを取得しています...")

        sub_thread: Thread = Thread(target=self._sub_thread)
        sub_thread.start()

    def on_modify_button_click(self) -> None:
        """「追加・更新」ボタンがクリックされたとき。"""

        if not self._validate_values():
            return

        record: dict[str, int | str] = {
            "times": int(self._text_times.text),
            "first_num": int(self._text_1st_num.text),
            "second_num": int(self._text_2nd_num.text),
            "third_num": int(self._text_3rd_num.text),
            "fourth_num": int(self._text_4th_num.text),
            "fifth_num": int(self._text_5th_num.text),
            "sixth_num": int(self._text_6th_num.text),
            "bonus_num": int(self._text_bonus_num.text),
            "ball_set": self._text_ball_set.text.upper(),
        }

        df: pl.DataFrame = self._shared_vars.df
        if df.filter(pl.col("times") == record["times"]).is_empty():
            self._append_record(record)
            self._display_status(f"'第 {record['times']} 回' のデータを追加しました。")
        else:
            times: int = int(record["times"])
            self._shared_vars.df = self._shared_vars.df.filter(pl.col("times") != times)
            self._append_record(record)
            self._display_status(f"'第 {record['times']} 回' のデータを更新しました。")

        self._selected_times = -1
        self._clear_input_text()

    def _clear_input_text(self) -> None:
        """すべての TextInput の入力値をクリアする。"""

        self._text_times.text = ""
        self._text_1st_num.text = ""
        self._text_2nd_num.text = ""
        self._text_3rd_num.text = ""
        self._text_4th_num.text = ""
        self._text_5th_num.text = ""
        self._text_6th_num.text = ""
        self._text_bonus_num.text = ""
        self._text_ball_set.text = ""

        self._text_times.focus = True

    def _validate_values(self) -> bool:
        """各 TextInput の入力値を検証する。

        Returns:
            bool: すべての入力値が正しい場合は True、そうでない場合は False。
        """

        if not self._validate_times(self._text_times.text):
            return self._error_handling(self._text_times, "'抽選回' ")
        elif not self._validate_number(self._text_1st_num.text):
            return self._error_handling(self._text_1st_num, "'1' の番号")
        elif not self._validate_number(self._text_2nd_num.text):
            return self._error_handling(self._text_2nd_num, "'2' の番号")
        elif not self._validate_number(self._text_3rd_num.text):
            return self._error_handling(self._text_3rd_num, "'3' の番号")
        elif not self._validate_number(self._text_4th_num.text):
            return self._error_handling(self._text_4th_num, "'4' の番号")
        elif not self._validate_number(self._text_5th_num.text):
            return self._error_handling(self._text_5th_num, "'5' の番号")
        elif not self._validate_number(self._text_6th_num.text):
            return self._error_handling(self._text_6th_num, "'6' の番号")
        elif not self._validate_number(self._text_bonus_num.text):
            return self._error_handling(self._text_bonus_num, "'B' の番号")
        elif not self._validate_ball_set(self._text_ball_set.text):
            return self._error_handling(self._text_ball_set, "'セット球' ")
        else:
            return True

    def on_delete_button_click(self) -> None:
        """「削除」ボタンがクリックされたとき。"""

        def delete_record(times: int) -> None:
            df: pl.DataFrame = self._shared_vars.df
            self._shared_vars.df = df.filter(pl.col("times") != times)
            self._display_data()

            self._selected_times = -1
            self._clear_input_text()
            self._display_status(f"'第 {times} 回' のデータを削除しました。")

        msg: str = f"'第 {self._selected_times} 回' のデータを削除しますか？"
        YesNoBox(
            message=msg,
            on_yes_callback=lambda: delete_record(self._selected_times),
            size_hint=(0.5, 0.4),
        ).show()

    def show_save_file_dialog(self) -> None:
        """ファイル保存ダイアログを表示する。"""

        def save_file_callback(file_paths: list[str]) -> None:
            if file_paths:
                try:
                    self._write_csv(file_paths[0])
                except Exception:
                    self._display_status("CSV ファイルを出力できませんでした。")
                else:
                    self._display_status("CSV ファイルを出力しました。")

        SaveFileDialog(
            callback=save_file_callback,
            title="CSV ファイルを出力",
            button_decision_text="出力",
            default_file_name="output.csv",
            with_extension="csv",
            filters={"CSV ファイル": ["*.csv"], "すべてのファイル": ["*"]},
        ).show()

    def _display_data(self) -> None:
        """データグリッドにデータを表示する。"""

        if self._shared_vars.df.is_empty():
            self._datagrid.data = []
        else:
            self._datagrid.data = self._shared_vars.df.to_dicts()
            self._datagrid.scroll_to_bottom()

    def _display_status(self, message: str) -> None:
        """GUI に現在の状況を表示する。

        Args:
            message (str): 表示するメッセージ。
        """

        if self._label_status:
            self._label_status.text = message

    ###############################################################################
    # Controllers

    def _read_parquet(self) -> pl.DataFrame:
        """Parquet 形式のファイルを読み込む。

        Returns:
            pl.DataFrame: 読み込んだデータフレーム。
        """

        file_path: str = constants.PARQUET_PATH

        try:
            df: pl.DataFrame = pl.read_parquet(file_path)
        except FileNotFoundError:
            self._display_status(f"'{file_path}' が見つかりません。")
            df: pl.DataFrame = pl.DataFrame()
        except Exception as e:
            self._display_status(f"'{file_path}' の読み込みに失敗しました。")
            df: pl.DataFrame = pl.DataFrame()
            print(e)
        else:
            self._display_status("Parquet ファイルを読み込みました。")

        return df

    def _write_parquet(self, df: pl.DataFrame) -> None:
        """データを Parquet 形式で保存する。

        Args:
            df (pl.DataFrame): 保存するデータフレーム。
        """

        df.write_parquet(constants.PARQUET_PATH, compression="zstd")

        self._display_status("Parquet ファイルを保存しました。")

    def _write_csv(self, file_path: str) -> None:
        """データを CSV 形式で出力する。

        Args:
            file_path (str): 出力先のファイルパス。

        Raises:
            Exception: CSV ファイルの書き込みに失敗した場合。
        """

        df: pl.DataFrame = self._shared_vars.df.select(
            [
                "times",
                "first_num",
                "second_num",
                "third_num",
                "fourth_num",
                "fifth_num",
                "sixth_num",
                "bonus_num",
                "ball_set",
            ]
        )

        try:
            df.write_csv(file_path, include_bom=True)
        except Exception as e:
            raise e

    def _append_numbers_column(self, df: pl.DataFrame) -> pl.DataFrame:
        """DataFrame に list 型の numbers 列と numbers_with_bonus 列を追加する。

        Args:
            df (pl.DataFrame): 元の DataFrame。

        Returns:
            pl.DataFrame: numbers 列と numbers_with_bonus 列が追加された DataFrame。
        """

        return df.with_columns(
            pl.concat_list(
                [
                    "first_num",
                    "second_num",
                    "third_num",
                    "fourth_num",
                    "fifth_num",
                    "sixth_num",
                ]
            ).alias("numbers")
        ).with_columns(
            pl.concat_list(
                [
                    "first_num",
                    "second_num",
                    "third_num",
                    "fourth_num",
                    "fifth_num",
                    "sixth_num",
                    "bonus_num",
                ]
            ).alias("numbers_with_bonus")
        )

    def _sub_thread(self) -> None:
        """非同期でウェブサイトからデータを取得する。"""

        prev_count: int = self._shared_vars.df.height

        records: list[dict[str, int | str]] = fetch_data_from_web()

        if not records:
            self._display_status("ウェブサイトからデータを取得できませんでした。")
            return

        schema: dict[str, Any] = {
            "times": pl.UInt32,
            "first_num": pl.UInt8,
            "second_num": pl.UInt8,
            "third_num": pl.UInt8,
            "fourth_num": pl.UInt8,
            "fifth_num": pl.UInt8,
            "sixth_num": pl.UInt8,
            "bonus_num": pl.UInt8,
            "ball_set": pl.Utf8,
        }
        df_fetched: pl.DataFrame = pl.DataFrame(records, schema=schema)
        df_fetched = self._append_numbers_column(df_fetched)
        self._shared_vars.df = (
            pl.concat([self._shared_vars.df, df_fetched], how="vertical")
            .unique(subset="times", keep="last")
            .select(pl.all().sort_by("times"))
        )
        self._display_data()

        count: int = self._shared_vars.df.height - prev_count
        self._display_status(f"ウェブサイトから {count} 件のデータを取得しました。")

    def _append_record(self, record: dict[str, int | str]) -> None:
        """新しいレコードを追加する。

        Args:
            record (dict[str, int | str]): 追加するレコード。
        """

        schema: dict[str, Any] = {
            "times": pl.UInt32,
            "first_num": pl.UInt8,
            "second_num": pl.UInt8,
            "third_num": pl.UInt8,
            "fourth_num": pl.UInt8,
            "fifth_num": pl.UInt8,
            "sixth_num": pl.UInt8,
            "bonus_num": pl.UInt8,
            "ball_set": pl.Utf8,
        }
        df_new: pl.DataFrame = pl.DataFrame(record, schema=schema)
        df_new = self._append_numbers_column(df_new)
        self._shared_vars.df = pl.concat([self._shared_vars.df, df_new]).select(
            pl.all().sort_by("times")
        )
        self._display_data()

    def _error_handling(self, text_input: TextInput, name: str) -> bool:
        """エラー処理を行う。

        Args:
            text_input (TextInput): エラーが発生したテキストボックス。
            name (str): エラーが発生した項目の名前。

        Returns:
            bool: エラーが発生した場合は False、そうでない場合は True。
        """

        self._display_status(f"{name}が正しくありません。")
        text_input.focus = True
        text_input.select_all()
        return False

    def _validate_times(self, value: str) -> bool:
        """回数の値を検証する。

        Args:
            value (str): 検証する回数の文字列。

        Returns:
            bool: 回数の値が正しい場合は True、そうでない場合は False。
        """

        if value == "":
            return False

        try:
            _ = int(value)
        except ValueError:
            return False
        else:
            return True

    def _validate_ball_set(self, value: str) -> bool:
        """セット球の値を検証する。

        Args:
            value (str): 検証するセット球の文字列。

        Returns:
            bool: セット球の値が正しい場合は True、そうでない場合は False。
        """

        if len(value) != 1:
            return False

        if value.upper() in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]:
            return True
        else:
            return False

    def _validate_number(self, value: str) -> bool:
        """番号の値を検証する。

        Args:
            value (str): 検証する番号の文字列。

        Returns:
            bool: 番号の値が正しい場合は True、そうでない場合は False。
        """

        if value == "":
            return False

        try:
            _ = int(value)
        except ValueError:
            return False
        else:
            return 0 < int(value) <= 43
