"""LightGBM 4.7.0用。学習時の列型とCSV欠損表現が確認できる場合だけ使う。"""
import csv

import lightgbm as lgb
import pandas as pd


def recover_csv(model, csv_path, *, categorical_columns, float64_columns, missing_token):
    if lgb.__version__ != "4.7.0":
        raise ValueError("この例の検証対象はLightGBM 4.7.0です")
    names = model.feature_name()
    if not isinstance(categorical_columns, list) or not categorical_columns:
        raise ValueError("学習時の全カテゴリ列名を確認して指定してください")
    if not isinstance(float64_columns, list) or not isinstance(missing_token, str):
        raise ValueError("float64列名のリストとCSV欠損表現を指定してください")
    declared = categorical_columns + float64_columns
    if len(set(declared)) != len(declared) or set(declared) != set(names):
        raise ValueError(f"列宣言不一致: missing={sorted(set(names) - set(declared))}, "
                         f"extra={sorted(set(declared) - set(names))}, "
                         f"duplicates={sorted({name for name in declared if declared.count(name) > 1})}")
    # メタデータの順序は、学習時のDataFrame列順にあるcategory列の順序。
    category_names = [name for name in names if name in categorical_columns]
    metadata = model.pandas_categorical  # 内部属性。列名は保存されていない。
    if not isinstance(metadata, list) or len(metadata) != len(category_names):
        raise ValueError("カテゴリ語彙メタデータがないか、指定列数と一致しません")
    vocabularies = {}
    for name, values in zip(category_names, metadata):
        if (not isinstance(values, list) or not values
                or not all(isinstance(value, str) for value in values)
                or len(set(values)) != len(values)):
            raise ValueError("対応対象は順序なしの文字列カテゴリです。語彙形式を確認してください")
        if missing_token in values:
            raise ValueError("CSV欠損表現がカテゴリ語彙と衝突しています")
        vocabularies[name] = values
    with open(csv_path, encoding="utf-8-sig", newline="") as stream:
        rows = csv.reader(stream, strict=True)
        header = next(rows, None)
        if not header:
            raise ValueError("CSVが空かヘッダーがありません")
        count = 0
        for row in rows:
            count += 1
            if len(row) != len(header):
                raise ValueError(f"CSVレコード{count + 1}の列数がヘッダーと一致しません")
        if count == 0:
            raise ValueError("CSVにデータ行がありません")
    if len(set(header)) != len(header) or set(header) != set(names):
        raise ValueError(f"CSV列不一致: missing={sorted(set(names) - set(header))}, "
                         f"extra={sorted(set(header) - set(names))}, "
                         f"duplicates={sorted({name for name in header if header.count(name) > 1})}")
    dtypes = {name: "string" for name in category_names}
    dtypes.update({name: "float64" for name in float64_columns})
    raw = pd.read_csv(csv_path, encoding="utf-8-sig", dtype=dtypes, keep_default_na=False,
                      na_values=[missing_token], float_precision="round_trip")
    unknown = {}
    for name, values in vocabularies.items():
        found = raw.loc[raw[name].notna() & ~raw[name].isin(values), name].unique().tolist()
        if found:
            unknown[name] = found
    if unknown:
        raise ValueError(f"未知カテゴリを拒否しました: {unknown}")
    restored = raw.loc[:, names].copy()
    for name, values in vocabularies.items():
        restored[name] = pd.Categorical(restored[name], categories=values, ordered=False)
    return restored
