# -*- coding: utf-8 -*-
# ~ Jesse K. Rubin ~ Pretty Useful Python
from itertools import chain
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple

from pupy.decorations import cash_it


class SodokuError(ValueError):
    """simple Sodoku error"""

    def __init__(self, message: str, row: None = None, col: None = None) -> None:
        self.message = message
        self.row, self.col = row, col
        super(SodokuError, self).__init__(message, row, col)


class Sodoku(object):
    """Sodoku

    [ 0,  1,  2,  3,  4,  5,  6,  7,  8]
    [ 9, 10, 11, 12, 13, 14, 15, 16, 17]
    [18, 19, 20, 21, 22, 23, 24, 25, 26]
    [27, 28, 29, 30, 31, 32, 33, 34, 35]
    [36, 37, 38, 39, 40, 41, 42, 43, 44]
    [45, 46, 47, 48, 49, 50, 51, 52, 53]
    [54, 55, 56, 57, 58, 59, 60, 61, 62]
    [63, 64, 65, 66, 67, 68, 69, 70, 71]
    [72, 73, 74, 75, 76, 77, 78, 79, 80]
    """

    def __init__(self, board: str) -> None:
        self.is_solved = False
        self.board = board.replace(".", "0")

    def solve(self) -> None:
        """Solve soduku

        :return: Nuthing... but the thing is solved in the obj
        """
        if 17 > sum(1 for n in self.board if n != "0"):
            raise SodokuError("not enough info")
        full_set = "123456789"
        d = {
            i: ("".join(c for c in full_set) if self.board[i] == "0" else self.board[i])
            for i in range(81)
        }
        d = Sodoku.update_dictionary(d)
        tf, d = Sodoku.reduce_dictionary(d)
        if not tf:
            raise SodokuError("check_unsolvable")
        a = [d[ind] for ind in range(81)]
        self.board = "".join(a)
        self.is_solved = True

    def euler_096_three_digit_number(self):
        """

        Returns:

        """
        if not self.is_solved:
            self.solve()
        return int(self.board[0:3])

    @staticmethod
    def first_unknown(d: Dict[int, str]) -> Optional[int]:
        """

        Args:
            d:

        Returns:

        """
        for i in range(81):
            if len(d[i]) > 1:
                return i

    @staticmethod
    def unsolvable(rcbd: Dict[str, List[int]]) -> bool:
        """

        Args:
            rcbd:

        Returns:

        """
        return any(len(v) == 0 for v in rcbd.values())

    @staticmethod
    def check_unsolvable(d: Dict[int, str]) -> Dict[int, str]:
        """

        Args:
            d:

        Returns:

        """
        nd = {k: v for k, v in d.items()}
        for rcb in range(9):
            box = {
                str(n): [
                    ind for ind in Sodoku.ibox(*divmod(rcb, 3)) if str(n) in d[ind]
                ]
                for n in range(1, 10)
            }
            row = {
                str(n): [ind for ind in Sodoku.irow(rcb) if str(n) in d[ind]]
                for n in range(1, 10)
            }
            col = {
                str(n): [
                    ind
                    for ind in Sodoku.icolumn(rcb)
                    if str(n) in d[ind] or str(n) == d[ind]
                ]
                for n in range(1, 10)
            }
            if (
                Sodoku.unsolvable(box)
                or Sodoku.unsolvable(row)
                or Sodoku.unsolvable(col)
            ):
                raise SodokuError("UNSOLVABLE")
        return nd

    @staticmethod
    def update_dictionary(d: Dict[int, str]) -> Dict[int, str]:
        """

        Args:
            d:

        Returns:

        """
        nd = {k: v for k, v in d.items()}
        for i in range(81):
            if len(nd[i]) == 1:
                for nay in Sodoku.neighbors(i):
                    if len(nd[nay]) != 1 and nd[i] in nd[nay]:
                        nd[nay] = nd[nay].replace(nd[i], "")
        return nd

    @staticmethod
    def reduce_dictionary(d: Dict[int, str]) -> Tuple[bool, Dict[int, str]]:
        """

        Args:
            d:

        Returns:

        """
        if all(len(v) == 1 for v in d.values()):
            return True, d
        try:
            d = Sodoku.check_unsolvable(d)
        except SodokuError:
            return False, d
        d = Sodoku.update_dictionary(d)
        if any(len(v) == 0 for k, v in d.items()):
            return False, d
        fz = Sodoku.first_unknown(d)
        if fz is None:
            if Sodoku.hasdup(d):
                return False, d
            return Sodoku.reduce_dictionary(d)
        for poss in d[fz]:
            nd = {k: v for k, v in d.items()}
            nd[fz] = str(poss)
            if not Sodoku.hasdup(nd):
                valid, ret = Sodoku.reduce_dictionary(nd)
                if valid:
                    return valid, ret
        return False, d

    def __str__(self) -> str:
        header = "  S   O   D   O   K   U  "
        top_border = "╔═══════╦═══════╦═══════╗"
        mid_border = "╠═══════╬═══════╬═══════╣"
        bot_border = "╚═══════╩═══════╩═══════╝"
        top_boxes = "\n".join(
            "║ {} {} {} ║ {} {} {} ║ {} {} {} ║".format(*self.board[l * 9 : l * 9 + 9])
            for l in range(0, 3)
        )
        mid_boxes = "\n".join(
            "║ {} {} {} ║ {} {} {} ║ {} {} {} ║".format(*self.board[l * 9 : l * 9 + 9])
            for l in range(3, 6)
        )
        bot_boxes = "\n".join(
            "║ {} {} {} ║ {} {} {} ║ {} {} {} ║".format(*self.board[l * 9 : l * 9 + 9])
            for l in range(6, 9)
        )
        strings = [
            header,
            top_border,
            top_boxes,
            mid_border,
            mid_boxes,
            mid_border,
            bot_boxes,
            bot_border,
        ]
        return "\n".join(strings)

    @staticmethod
    def hasdup(d: Dict[int, str]) -> bool:
        """

        Args:
            d:

        Returns:

        """
        for i in range(81):
            if len(d[i]) == 1:
                for n in Sodoku.neighbors(i):
                    if d[n] == d[i]:
                        return True
        return False

    def get_oneline_str(self) -> str:
        """

        Returns:

        """
        return self.board

    @staticmethod
    def neighbors(index: int, size: int = 9) -> Set[int]:
        """

        Args:
            index:
            size:

        Returns:

        """
        return {
            ni
            for ni in chain(
                Sodoku.irow(index // size),
                Sodoku.icolumn(index % size),
                Sodoku.box_box(index),
            )
        } - {index}

    @staticmethod
    def irow(n: int, bsize: int = 9) -> Set[int]:
        """

        Args:
            n:
            bsize:

        Returns:

        """
        return {i for i in range(n * bsize, n * bsize + bsize)}

    @staticmethod
    def icolumn(n: int, bsize: int = 9) -> Set[int]:
        """

        Args:
            n:
            bsize:

        Returns:

        """
        return {i for i in range(n, bsize ** 2, bsize)}

    @staticmethod
    def ibox(box_r: int, box_c: int, bsize: int = 9) -> Set[int]:
        """

        Args:
            box_r:
            box_c:
            bsize:

        Returns:

        """
        return {
            i * bsize + j
            for i in range((box_r * 3), (box_r * 3) + 3)
            for j in range((box_c * 3), (box_c * 3) + 3)
        }

    @staticmethod
    @cash_it
    def box_box(index: int) -> Set[int]:
        """

        Args:
            index:
            bsize:

        Returns:

        """
        for box_r in range(3):
            for box_c in range(3):
                box = Sodoku.ibox(box_r, box_c)
                if index in box:
                    return box
