import argparse
import json
import os
import pickle
import torch
from tqdm import tqdm
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer

from lib.tokenize_data_mask import apply_chat_with_tokenize_with_mark


CUSTOM_MASK_IDENTIFIER = {
    "data": ["<data>", "</data>"],
    "inst": ["<inst>", "</inst>"],
}


class SepDataset:
    name = "sep"

    def __init__(self, path: str):
        self.path = path
        self.records = []
        self._load()

    def _load(self):
        with open(self.path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                prompts = json.loads(line)
                if not isinstance(prompts, list) or len(prompts) < 2:
                    raise ValueError("Each SEP line must be a JSON list with two prompts.")
                self.records.append(prompts)

    def iter_records(self):
        for idx, prompts in enumerate(self.records):
            yield idx, prompts

    @staticmethod
    def build_metric_masks(instruction_mask, segment_type, is_normal_token, custom_mask):
        base = [
            (seg == "usr") and norm and (cust == "inst")
            for seg, norm, cust in zip(segment_type, is_normal_token, custom_mask)
        ]
        instr = [b and im for b, im in zip(base, instruction_mask)]
        return {
            "sep_native": base,
            "sep_instrtive": instr,
        }


def load_model(model_path):
    cfg = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
    tok = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
    if tok.pad_token_id is None:
        tok.pad_token = tok.eos_token
        tok.pad_token_id = tok.eos_token_id
        tok.padding_side = "right"
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        config=cfg,
        device_map="auto",
        torch_dtype=torch.bfloat16,
        trust_remote_code=True,
        attn_implementation="eager",
    )
    model.eval()
    return model, tok


@torch.inference_mode()
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", required=True)
    parser.add_argument("--dataset-sep", default=None)
    parser.add_argument("--output-dir", required=True)
    parser.add_argument("--split-size", type=int, default=100)
    parser.add_argument("--max-data", type=int, default=None)
    args = parser.parse_args()

    model, tok = load_model(args.model)
    n_layers = getattr(model.config, "num_hidden_layers", None)
    n_heads = getattr(model.config, "num_attention_heads", None)
    if n_layers is None or n_heads is None:
        raise ValueError("Model config missing num_hidden_layers or num_attention_heads.")
    datasets = []
    if args.dataset_sep:
        datasets.append(SepDataset(args.dataset_sep))

    os.makedirs(args.output_dir, exist_ok=True)
    split_idx = 0
    raw_output = None
    total_prompts = sum(len(prompts) for dataset in datasets for _rid, prompts in dataset.iter_records())
    if total_prompts == 0:
        raise ValueError("No prompts loaded. Please provide at least one dataset.")

    remaining_records = args.max_data
    for dataset in datasets:
        record_iter = list(dataset.iter_records())
        if remaining_records is not None:
            record_iter = record_iter[:remaining_records]
        all_prompts = [
            (record_id, prompt)
            for record_id, prompts in record_iter
            for prompt in prompts
        ]
        for record_id, prompt in tqdm(
            all_prompts,
            desc=f"Processing {dataset.name} dataset",
            leave=True,
        ):
            if raw_output is None:
                raw_output = {
                    "prompts": [],
                    "heads": {
                        f"L{l}_H{h}": {
                            "attn_weight": [],
                        }
                        for l in range(n_layers)
                        for h in range(n_heads)
                    },
                }
            messages = [
                {"role": "system", "content": ""},
                {"role": "user", "content": prompt},
            ]
            (
                input_ids,
                instruction_mask,
                _data_mask,
                segment_type,
                is_normal_token,
                custom_mask,
                _rendered,
            ) = apply_chat_with_tokenize_with_mark(
                messages,
                tok,
                custom_mask_identifier=CUSTOM_MASK_IDENTIFIER,
                add_generation_prompt=True,
            )
            input_ids_tensor = torch.tensor([input_ids], dtype=torch.long, device=model.device)
            attention_mask = torch.ones_like(input_ids_tensor)
            out = model(
                input_ids=input_ids_tensor,
                attention_mask=attention_mask,
                output_attentions=True,
            )
            attn = out.attentions  # tuple layers: (B, H, T, S)
            user_mask = [seg == "usr" for seg in segment_type]
            inst_mask = [cust == "inst" for cust in custom_mask]
            instr_mask = instruction_mask
            raw_output["prompts"].append(
                {
                    "token_ids": input_ids,
                    "user_mask": user_mask,
                    "inst_mask": inst_mask,
                    "instr_mask": instr_mask,
                }
            )
            for l in range(n_layers):
                layer_attn = attn[l][0]
                for h in range(n_heads):
                    head_key = f"L{l}_H{h}"
                    head_store = raw_output["heads"][head_key]
                    head_store["attn_weight"].append(
                        layer_attn[h].to(torch.float16).cpu().numpy()[-1, :]
                    )
            del out, attn, input_ids_tensor, attention_mask, layer_attn
            if len(raw_output["prompts"]) >= args.split_size:
                output_path = os.path.join(
                    args.output_dir, f"head_scoring_raw_split_{split_idx}.pkl"
                )
                with open(output_path, "wb") as f:
                    pickle.dump(raw_output, f)
                split_idx += 1
                raw_output = None
        if remaining_records is not None:
            remaining_records -= len(record_iter)
            if remaining_records <= 0:
                break

    if raw_output is not None and raw_output["prompts"]:
        output_path = os.path.join(
            args.output_dir, f"head_scoring_raw_split_{split_idx}.pkl"
        )
        with open(output_path, "wb") as f:
            pickle.dump(raw_output, f)


if __name__ == "__main__":
    main()
