from __future__ import annotations

import os
import platform
import shutil
import sqlite3
import sys
import tempfile
import textwrap
from contextlib import closing
from pathlib import Path
from time import monotonic
from typing import Any, Optional

import zstd

from chik.util.config import load_config, lock_and_load_config, save_config
from chik.util.db_wrapper import get_host_parameter_limit
from chik.util.path import path_from_root


# if either the input database or output database file is specified, the
# configuration file will not be updated to use the new database. Only when using
# the currently configured db file, and writing to the default output file will
# the configuration file also be updated
def db_upgrade_func(
    root_path: Path,
    in_db_path: Optional[Path] = None,
    out_db_path: Optional[Path] = None,
    *,
    no_update_config: bool = False,
    force: bool = False,
) -> None:
    update_config: bool = in_db_path is None and out_db_path is None and not no_update_config

    config: dict[str, Any]
    selected_network: str
    db_pattern: str
    if in_db_path is None or out_db_path is None:
        config = load_config(root_path, "config.yaml")["full_node"]
        selected_network = config["selected_network"]
        db_pattern = config["database_path"]

    db_path_replaced: str
    if in_db_path is None:
        db_path_replaced = db_pattern.replace("CHALLENGE", selected_network)
        in_db_path = path_from_root(root_path, db_path_replaced)

    if out_db_path is None:
        db_path_replaced = db_pattern.replace("CHALLENGE", selected_network).replace("_v1_", "_v2_")
        out_db_path = path_from_root(root_path, db_path_replaced)
        out_db_path.parent.mkdir(parents=True, exist_ok=True)

    _, _, free = shutil.disk_usage(out_db_path.parent)
    in_db_size = in_db_path.stat().st_size
    if free < in_db_size:
        no_free: bool = free < in_db_size * 0.6
        strength: str
        if no_free:
            strength = "probably not enough"
        else:
            strength = "very little"
        print(f"there is {strength} free space on the volume where the output database will be written:")
        print(f"   {out_db_path}")
        print(
            f"free space: {free / 1024 / 1024 / 1024:0.2f} GiB expected about "
            f"{in_db_size / 1024 / 1024 / 1024:0.2f} GiB"
        )
        if no_free and not force:
            print("to override this check and convert anyway, pass --force")
            return

    try:
        convert_v1_to_v2(in_db_path, out_db_path)

        if update_config:
            print("updating config.yaml")
            with lock_and_load_config(root_path, "config.yaml") as config:
                new_db_path = db_pattern.replace("_v1_", "_v2_")
                config["full_node"]["database_path"] = new_db_path
                print(f"database_path: {new_db_path}")
                save_config(root_path, "config.yaml", config)

    except RuntimeError as e:
        print(f"conversion failed with error: {e}.")
    except Exception as e:
        print(
            textwrap.dedent(
                f"""\
            conversion failed with error: {e}.
            The target v2 database is left in place (possibly in an incomplete state)
              {out_db_path}
            If the failure was caused by a full disk, ensure the volumes of your
            temporary- and target directory have sufficient free space."""
            )
        )
        if platform.system() == "Windows":
            temp_dir = None
            # this is where GetTempPath() looks
            # https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha
            if "TMP" in os.environ:
                temp_dir = os.environ["TMP"]
            elif "TEMP" in os.environ:
                temp_dir = os.environ["TEMP"]
            elif "USERPROFILE" in os.environ:
                temp_dir = os.environ["USERPROFILE"]
            if temp_dir is not None:
                print(f"your temporary directory may be {temp_dir}")
            temp_env = "TMP"
        else:
            temp_env = "SQLITE_TMPDIR"
        print(f'you can specify the "{temp_env}" environment variable to control the temporary directory to be used')

    print(f"\n\nLEAVING PREVIOUS DB FILE UNTOUCHED {in_db_path}\n")


def convert_v1_to_v2(in_path: Path, out_path: Path) -> None:
    BATCH_SIZE = 300_000
    if not in_path.exists():
        raise RuntimeError(f"Input file doesn't exist. {in_path}")
    if in_path == out_path:
        raise RuntimeError(f"Output file is the same as the input {in_path}")
    if out_path.exists():
        raise RuntimeError(f"Output file already exists. {out_path}")

    print(f"-- Opening file for reading: {in_path}")
    with sqlite3.connect(in_path) as conn:
        try:
            with closing(conn.execute("SELECT version FROM database_version LIMIT 1")) as cursor:
                row = cursor.fetchone()
            if row is not None and row[0] != 1:
                raise RuntimeError(f"Blockchain database already version {row[0]}. Won't convert")
        except sqlite3.OperationalError:
            pass
        try:
            conn.execute("SELECT unhex('00')")
        except sqlite3.OperationalError:
            print("-- No built-in unhex(), falling back to bytes.fromhex()")
            conn.create_function("unhex", 1, bytes.fromhex)
        conn.create_function("zstd_compress", 1, zstd.compress)
        conn.execute("PRAGMA synchronous=off")
        temp_dir = tempfile.gettempdir()
        _, _, free = shutil.disk_usage(temp_dir)
        if free < 50 * 1024 * 1024 * 1024:
            print(f"-- Setting temp_store_directory to: {out_path.parent}")
            conn.execute(f"PRAGMA temp_store_directory = '{out_path.parent}'")
        print(f"-- Opening file for writing: {out_path}")
        conn.execute("ATTACH DATABASE ? AS out_db", (str(out_path),))
        conn.execute("PRAGMA out_db.journal_mode=off")
        conn.execute("PRAGMA out_db.synchronous=off")
        conn.execute("PRAGMA out_db.locking_mode=exclusive")
        conn.execute("PRAGMA out_db.cache_size=131072")
        print("-- Initializing v2 version")
        conn.execute("CREATE TABLE out_db.database_version(version int)")
        conn.execute("INSERT INTO out_db.database_version VALUES(?)", (2,))
        print("-- Initializing current_peak")
        conn.execute("CREATE TABLE out_db.current_peak(key int PRIMARY KEY, hash blob)")
        with closing(conn.execute("SELECT header_hash, height FROM block_records WHERE is_peak = 1 LIMIT 1")) as cursor:
            peak_row = cursor.fetchone()
        if peak_row is None:
            raise RuntimeError("v1 database does not have a peak block, there is no blockchain to convert")
        peak_hash, peak_height = peak_row
        print(f"-- Peak: {peak_hash} Height: {peak_height}")
        conn.execute("INSERT INTO out_db.current_peak VALUES(?, ?)", (0, bytes.fromhex(peak_hash)))
        conn.commit()
        print("-- DB v1 to v2 conversion started")
        print("-- [1/4] Converting full_blocks")
        conn.execute(
            """
            CREATE TABLE out_db.full_blocks(
                header_hash blob PRIMARY KEY,
                prev_hash blob,
                height bigint,
                sub_epoch_summary blob,
                is_fully_compactified tinyint,
                in_main_chain tinyint,
                block blob,
                block_record blob
            )
            """
        )
        conn.commit()
        parameter_limit = get_host_parameter_limit()
        start_time = monotonic()
        block_start_time = start_time
        rowids: list[int] = []
        small_batch_size = BATCH_SIZE <= parameter_limit
        small_chain = peak_height <= parameter_limit
        current_header_hash = peak_hash
        current_height = peak_height
        insertions_vs_batch = 0
        rate = 1.0
        while current_height >= 0:
            while len(rowids) < parameter_limit:
                if current_height < 0:
                    break
                with closing(
                    conn.execute(
                        "SELECT rowid, prev_hash FROM block_records WHERE header_hash = ? AND height = ? LIMIT 1",
                        (current_header_hash, current_height),
                    )
                ) as cursor:
                    row = cursor.fetchone()
                if row is None:
                    raise RuntimeError(f"rowid not found for {current_header_hash} at height {current_height}")
                rowid, prev_hash = row
                rowids.append(rowid)
                current_header_hash = prev_hash
                current_height -= 1
            conn.execute(
                f"""
                INSERT INTO out_db.full_blocks
                    SELECT
                        unhex(br.header_hash),
                        unhex(br.prev_hash),
                        br.height, br.sub_epoch_summary,
                        fb.is_fully_compactified,
                        1 AS in_main_chain,
                        zstd_compress(fb.block),
                        br.block
                    FROM block_records br
                    JOIN full_blocks fb ON br.header_hash = fb.header_hash
                    WHERE br.rowid IN ({','.join('?' * len(rowids))})
                """,
                rowids,
            )
            insertions_vs_batch += len(rowids)
            rowids = []
            if insertions_vs_batch >= BATCH_SIZE or small_batch_size or small_chain:
                conn.commit()
                end_time = monotonic()
                rate = BATCH_SIZE / (end_time - start_time)
                print(
                    f"\r{current_height:10d} {(peak_height - current_height) * 100 / peak_height:.3f}% "
                    f"{rate:0.1f} blocks/s ETA: {current_height // rate} s    ",
                    end="",
                )
                sys.stdout.flush()
                start_time = end_time
                insertions_vs_batch = 0
        end_time = monotonic()
        print(
            "\r-- [1/4] Converting full_blocks SUCCEEDED in "
            f"{end_time - block_start_time:.2f} seconds                             "
        )
        print("-- [1/4] Creating full_blocks height index")
        height_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.height ON full_blocks(height)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [1/4] Creating full_blocks height index SUCCEEDED in "
            f"{end_time - height_index_start_time:.2f} seconds                             "
        )
        print("-- [1/4] Creating full_blocks is_fully_compactified index")
        ifc_index_start_time = monotonic()
        conn.execute(
            """
            CREATE INDEX out_db.is_fully_compactified
                ON full_blocks(is_fully_compactified, in_main_chain)
                WHERE in_main_chain=1
            """
        )
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [1/4] Creating full_blocks is_fully_compactified index SUCCEEDED in "
            f"{end_time - ifc_index_start_time:.2f} seconds                             "
        )
        print("-- [1/4] Creating full_blocks main_chain index")
        main_chain_index_start_time = monotonic()
        conn.execute(
            """
            CREATE INDEX out_db.main_chain
                ON full_blocks(height, in_main_chain)
                WHERE in_main_chain=1
            """
        )
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [1/4] Creating full_blocks main_chain index SUCCEEDED in "
            f"{end_time - main_chain_index_start_time:.2f} seconds                             "
        )
        print("-- [2/4] Converting sub_epoch_segments_v3")
        conn.execute(
            """
            CREATE TABLE out_db.sub_epoch_segments_v3(
                ses_block_hash blob PRIMARY KEY,
                challenge_segments blob
            )
            """
        )
        conn.commit()
        ses_start_time = monotonic()
        conn.execute(
            """
            INSERT INTO out_db.sub_epoch_segments_v3
                SELECT
                    unhex(ses_block_hash),
                    challenge_segments
                FROM sub_epoch_segments_v3
            """
        )
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [2/4] Converting sub_epoch_segments_v3 SUCCEEDED in "
            f"{end_time - ses_start_time:.2f} seconds                             "
        )
        print("-- [3/4] Converting hints")
        conn.execute("CREATE TABLE out_db.hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))")
        conn.commit()
        start_time = monotonic()
        hint_start_time = start_time
        hints_count = 0
        rate = 1.0
        range_start = 1
        while True:
            with closing(
                conn.execute("SELECT id FROM hints WHERE id >= ? LIMIT ?", (range_start, parameter_limit))
            ) as cursor:
                rows = cursor.fetchall()
            if len(rows) == 0:
                break
            conn.execute(
                f"""
                INSERT OR IGNORE INTO out_db.hints
                    SELECT
                        coin_id,
                        hint
                    FROM hints
                    WHERE id IN ({','.join('?' * len(rows))})
                """,
                [id_tuple[0] for id_tuple in rows],
            )
            conn.commit()
            end_time = monotonic()
            rate = parameter_limit / (end_time - start_time)
            start_time = end_time
            hints_count += len(rows)
            print(f"\r{hints_count // 1000:10d}k hints {rate:0.1f} hints/s  ", end="")
            sys.stdout.flush()
            range_start += parameter_limit
        end_time = monotonic()
        print(
            "\r-- [3/4] Converting hints SUCCEEDED in "
            f"{end_time - hint_start_time:.2f} seconds                             "
        )
        print("-- [3/4] Creating hints hint_index index")
        hint_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.hint_index on hints(hint)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [3/4] Creating hints hint_index index SUCCEEDED in "
            f"{end_time - hint_index_start_time:.2f} seconds                             "
        )
        print("-- [4/4] Converting coin_record")
        conn.execute(
            """
            CREATE TABLE out_db.coin_record(
                coin_name blob PRIMARY KEY,
                confirmed_index bigint,
                spent_index bigint,
                coinbase int,
                puzzle_hash blob,
                coin_parent blob,
                amount blob,
                timestamp bigint
            )
            """
        )
        conn.commit()
        start_time = monotonic()
        coin_start_time = start_time
        coins_count = 0
        rate = 1.0
        print("-- [4/4] Creating temp table (slow, patience)", end="\r")
        conn.execute(
            """
            CREATE TABLE temp.coin_record AS
            SELECT
                unhex(coin_name),
                confirmed_index,
                CASE WHEN spent_index <= ? THEN spent_index ELSE 0 END AS spent_index,
                coinbase,
                unhex(puzzle_hash),
                unhex(coin_parent),
                amount,
                timestamp
            FROM coin_record
            WHERE confirmed_index <= ?
            ORDER BY unhex(coin_name)
            """,
            (peak_height, peak_height),
        )
        with closing(conn.execute("SELECT * FROM temp.coin_record")) as cursor:
            while True:
                rows = cursor.fetchmany(BATCH_SIZE)
                if len(rows) == 0:
                    break
                conn.executemany("INSERT INTO out_db.coin_record VALUES (?, ?, ?, ?, ?, ?, ?, ?)", rows)
                conn.commit()
                end_time = monotonic()
                rate = len(rows) / (end_time - start_time)
                start_time = end_time
                coins_count += len(rows)
                print(f"\r{coins_count // 1000:10d}k coins {rate:0.1f} coins/s               ", end="")
                sys.stdout.flush()
        end_time = monotonic()
        print(
            "\r-- [4/4] Converting coin_record SUCCEEDED in "
            f"{end_time - coin_start_time:.2f} seconds                             "
        )
        print("-- [4/4] Creating coin_record coin_confirmed_index index")
        coin_confirmed_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.coin_confirmed_index ON coin_record(confirmed_index)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [4/4] Creating coin_record coin_confirmed_index index SUCCEEDED in "
            f"{end_time - coin_confirmed_index_start_time:.2f} seconds                             "
        )
        print("-- [4/4] Creating coin_record coin_spent_index index")
        coin_spent_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.coin_spent_index ON coin_record(spent_index)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [4/4] Creating coin_record coin_spent_index index SUCCEEDED in "
            f"{end_time - coin_spent_index_start_time:.2f} seconds                             "
        )
        print("-- [4/4] Creating coin_record coin_puzzle_hash index")
        coin_puzzle_hash_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.coin_puzzle_hash ON coin_record(puzzle_hash)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [4/4] Creating coin_record coin_puzzle_hash index SUCCEEDED in "
            f"{end_time - coin_puzzle_hash_index_start_time:.2f} seconds                             "
        )
        print("-- [4/4] Creating coin_record coin_parent_index index")
        coin_parent_index_start_time = monotonic()
        conn.execute("CREATE INDEX out_db.coin_parent_index ON coin_record(coin_parent)")
        conn.commit()
        end_time = monotonic()
        print(
            "\r-- [4/4] Creating coin_record coin_parent_index index SUCCEEDED in "
            f"{end_time - coin_parent_index_start_time:.2f} seconds                             "
        )
        conn.execute("DETACH DATABASE out_db")
