import fix_ulimit
from train_target import EMBED

fix_ulimit.fix_ulimit()

import queue
import threading
from dataclasses import dataclass
from typing import List
import torch
from tqdm import tqdm
import dataclasses
import json
import numpy as np
from pgvector.psycopg import register_vector

import typeguard
from model import ModelArgs, MusicalPositionEmbedTransformer
from data import Vocab
from data_gen import DataGenerator, Dataset, TrivialAugmenter
import dataset_classes
import psycopg
import fire
import torch.distributed as dist
from classifier_model import SimpleClassifier


def embed_lanes(config_path: str, model_path: str, batch_size: int):
    dist.init_process_group("nccl")
    rank = dist.get_rank()
    world_size = dist.get_world_size()
    device_id = rank % torch.cuda.device_count()
    device_str = f"cuda:{device_id}"
    torch.cuda.set_device(device_id)

    logging.info(f"start rank {rank+1} of {world_size}")

    with open(config_path, "r") as f:
        config = json.load(f)

    vocab = Vocab.from_config(config)

    conn_str = config["dataset_class"]["conn_str"]

    seq_len = int(config["seq_len"])
    seq_len_min = int(config["seq_len_min"])
    seq_len_max = int(config["seq_len_max"])

    model = MusicalPositionEmbedTransformer(
        vocab,
        dataclasses.replace(
            ModelArgs.from_config(vocab, config),
            max_batch_size=batch_size,
            max_seq_len=seq_len,
            cache=False,
        ),
    )
    model.load_state_dict(
        torch.load(
            model_path,
            map_location=torch.device("cpu"),
        )
    )

    model = model.to(device_str)

    dataset = Dataset.dynamic_from_config(config, "dataset_class")

    data_gen = DataGenerator(
        dataset,
        vocab,
        split=None,
        ranksize=(rank, world_size),
        batch_size=batch_size,
        seq_len=seq_len,
        seq_len_min=seq_len_min,
        seq_len_max=seq_len_max,
        train_target=EMBED,
        parallelism=8,
        to_device=device_str,
        text_tokenize=None,
        example_continuous=True,
        pack_batch=False,
        augmenter=TrivialAugmenter(),
    )

    q_batches = queue.Queue(maxsize=10)
    q_eval_out = queue.Queue(maxsize=100)

    def thd_batchify_entry():
        for batch in tqdm(
            data_gen.generate(order="random"),
            total=int(data_gen.num_examples() / (batch_size * world_size)),
            disable=(rank != 0),
        ):
            q_batches.put(batch)
        q_batches.put(None)

    thd_batchify = threading.Thread(target=thd_batchify_entry, daemon=True)
    thd_batchify.start()

    def thd_eval_entry():
        model.eval()
        with torch.no_grad():
            while True:
                batch = q_batches.get()
                if batch is None:
                    break
                ys, tok_counts = model.forward(
                    batch.tgt, start_pos=0, return_embeddings=True
                )
                q_eval_out.put((batch.ids, ys, tok_counts))
            q_eval_out.put(None)

    thd_eval = threading.Thread(target=thd_eval_entry, daemon=True)
    thd_eval.start()

    @dataclass
    class EmbedRec:
        id: int
        y: np.ndarray
        avg_count: int

    embed_avgs: dict[int, EmbedRec] = dict()

    with psycopg.connect(conn_str) as conn:
        register_vector(conn)
        conn.autocommit = True
        finished_embeds: List[EmbedRec] = []
        with conn.cursor() as cur:

            def flush(to_empty):
                if len(finished_embeds) > (0 if to_empty else 100):
                    with conn.transaction():
                        cur.executemany(
                            """
                            WITH embedding_ids AS (
                                INSERT INTO embeddings (embedding)
                                VALUES (%s)
                                RETURNING %s as lane_id, id as embedding_id
                            ) INSERT INTO lane_embeddings
                                (lane_id, embedding_id)
                                SELECT lane_id, embedding_id FROM embedding_ids
                            """,
                            ((e.y, e.id) for e in finished_embeds),
                        )
                    finished_embeds.clear()

            while True:
                x = q_eval_out.get()
                if x is None:
                    break
                ids, ys, tok_counts = x
                this_batch_embeds = [
                    EmbedRec(id, y * num_toks, num_toks)
                    for id, y, num_toks in zip(
                        ids,
                        ys.cpu().detach().numpy(),
                        tok_counts.cpu().detach().numpy(),
                    )
                    if num_toks > seq_len_min - 1
                ]
                this_batch_ids = set(e.id for e in this_batch_embeds)
                for e in this_batch_embeds:
                    old = embed_avgs.get(e.id)
                    if old is None:
                        embed_avgs[e.id] = e
                    else:
                        old.y += e.y
                        old.avg_count += e.avg_count
                for id in embed_avgs.keys() - this_batch_ids:
                    e = embed_avgs[id]
                    del embed_avgs[id]
                    e.y /= e.avg_count
                    e.avg_count = 1
                    finished_embeds.append(e)
                flush(False)
            flush(True)

    thd_batchify.join()
    thd_eval.join()


def scratch():
    # gather some lane-embeddings that are either drums or not drums
    with psycopg.connect("host=localhost dbname=composer_new_dataset_v3") as conn:
        register_vector(conn)
        cur = conn.cursor()
        cur.execute(
            """
            select e.embedding, exists(
                    select from lane_tags lt
                    join tag_values tv on lt.tag_value_id = tv.id
                    where lt.lane_id = le.lane_id
                    and tv.value in ('Flagged Drums', 'Analyzed Drums', 'ML Analyzed Drums')
            ) as is_drum
            from embeddings e
            join lane_embeddings le on e.id = le.embedding_id
            join lanes l on l.id = le.lane_id
            join instruments i on i.id = l.instrument_id
            join files f on f.id = i.file_id
            and exists(
                select from lane_tags lt
                join tag_values tv on lt.tag_value_id = tv.id
                where lt.lane_id = le.lane_id
                and tv.value in ('Flagged Drums', 'Analyzed Drums', 'ML Analyzed Drums', 'ML Analyzed Non-Drums')
            )
            order by f.rand_order
            limit 100000
            """
        )
        xs = cur.fetchall()
        drums = [x for x, is_drum in xs if is_drum]
        not_drums = [x for x, is_drum in xs if not is_drum]
        # not_drums = random.sample(not_drums, len(drums))

        # train a SVM classifier
        from sklearn.svm import SVC
        from sklearn.model_selection import train_test_split
        from sklearn.metrics import classification_report

        X = np.array(drums + not_drums)
        y = np.array([1] * len(drums) + [0] * len(not_drums))
        # y = np.random.permutation(y)  # confuse
        model = SVC(class_weight="balanced")
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

        print("baseline:")
        print(classification_report(y_test, [0] * len(y_test)))

        # model.fit(X_train, y_train)
        # y_pred = model.predict(X_test)
        # print("svm:")
        # print(classification_report(y_test, y_pred))

        model2 = SimpleClassifier()
        optim = torch.optim.SGD(model2.parameters(), lr=0.01)
        loss = torch.nn.BCELoss()
        for i in range(1000):
            drum_idxs = np.nonzero(y_train == 1)[0]
            not_drum_idxs = np.nonzero(y_train == 0)[0]
            some_drums = random.sample(drum_idxs.tolist(), 16)
            some_not_drums = random.sample(not_drum_idxs.tolist(), 16)
            X_epoch = torch.tensor(
                np.concatenate([X_train[some_drums], X_train[some_not_drums]])
            )
            y_epoch = torch.tensor([1] * 16 + [0] * 16)
            y_pred = model2(X_epoch).squeeze()
            l = loss(y_pred, y_epoch.float())
            optim.zero_grad()
            l.backward()
            optim.step()

        with torch.no_grad():
            y_pred = model2.forward(torch.tensor(X_test)).squeeze().numpy() > 0.90
        print("logistic:")
        print(classification_report(y_test, y_pred))


def parse_bool(s):
    if s.lower() in ("true", "1"):
        return True
    elif s.lower() in ("false", "0"):
        return False
    else:
        raise ValueError(f"Cannot convert to bool: {s}")


class ClassifierTool:
    @typeguard.typechecked
    def embed_lanes(
        self,
        config_path: str,
        model_path: str,
        batch_size: int = 16,
    ):
        """
        Generate and write to the database embedding vectors for all lanes lacking them

        Model and vocab config will be read from config_path
        Model will be loaded from model_path, which should be stripped (see loader.py)
        Database connection string will be read from config["dataset_class"]["conn_str"]

        Should be invoked with torchrun --standalone --nproc-per-node gpu
        """
        embed_lanes(config_path, model_path, batch_size)

    def scratch(self):
        scratch()

    # @fire.decorators.SetParseFn(str, "class_tags", "nonclass_tags")
    # @typeguard.typechecked
    # def collect(
    #     self,
    #     class_tags: str,
    #     nonclass_tags: str,
    #     class_limit: Optional[int] = None,
    #     nonclass_multiplier: int = 1,
    #     examples_out: str = "data.pt",
    # ):
    #     """
    #     Collects examples for training the classifier.
    #     """
    #     collect_examples(
    #         [TagFilter.from_str(s) for s in class_tags.split(",")],
    #         class_limit,
    #         [TagFilter.from_str(s) for s in nonclass_tags.split(",")],
    #         nonclass_multiplier,
    #         examples_out,
    #     )

    # @typeguard.typechecked
    # def train(
    #     self,
    #     examples: str = "data.pt",
    #     batch_size: int = 16,
    #     train_epochs: int = 15,
    #     classifier_out: str = "drum_classifier.pt",
    #     val_split: float = 0.2,
    # ):
    #     """
    #     Trains the classifier.
    #     """
    #     train(examples, batch_size, train_epochs, classifier_out, val_split)

    # @fire.decorators.SetParseFn(str, "high_category_tag", "low_category_tag")
    # @fire.decorators.SetParseFn(parse_bool, "lane_mode")
    # @typeguard.typechecked
    # def classify(
    #     self,
    #     classifier: str = "drum_classifier.pt",
    #     batch_size: int = 100,
    #     high_category_tag: Optional[str] = "ML Analyzed Drums",
    #     low_category_tag: Optional[str] = "ML Analyzed Non-Drums",
    #     threshold: float = 0.5,
    #     lane_mode: bool = True,
    #     max_clips_per_lane: int = 8,
    #     limit: Optional[int] = None,
    # ):
    #     """
    #     Classifies lanes.
    #     """
    #     classify(
    #         classifier,
    #         batch_size,
    #         high_category_tag,
    #         low_category_tag,
    #         threshold,
    #         lane_mode,
    #         max_clips_per_lane,
    #         limit,
    #     )


if __name__ == "__main__":
    from util import configure_logging

    configure_logging()

    fire.Fire(ClassifierTool())
