#!/usr/bin/env python3
"""表示列ブックの1行目の書式と列幅を、志願者データへ反映する。"""

from __future__ import annotations

import argparse
import logging
import sys
from collections import defaultdict
from copy import copy
from pathlib import Path

from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.dimensions import ColumnDimension


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "参照Excelのラベルと完全一致する志願者データの列について、"
            "列幅とヘッダーセルの書式をコピーします。"
        )
    )
    parser.add_argument("input", type=Path, help="志願者データExcelファイル")
    parser.add_argument(
        "-r", "--reference", type=Path, default=Path("表示列.xlsx"),
        help="書式参照Excelファイル（既定: 表示列.xlsx）",
    )
    parser.add_argument("-o", "--output", type=Path, help="出力Excelファイル")
    parser.add_argument(
        "--log", type=Path,
        help="ログファイル（省略時: 出力ファイルと同じ場所の apply_display.log）",
    )
    parser.add_argument(
        "--input-sheet", help="志願者データのシート名（省略時はアクティブシート）"
    )
    parser.add_argument(
        "--reference-sheet", help="参照元のシート名（省略時はアクティブシート）"
    )
    parser.add_argument(
        "--header-row", type=int, default=1, help="ヘッダー行番号（既定: 1）"
    )
    return parser.parse_args()


def select_sheet(workbook, name: str | None, role: str):
    if name is None:
        return workbook.active
    if name not in workbook.sheetnames:
        names = ", ".join(repr(s) for s in workbook.sheetnames)
        raise ValueError(f"{role}シート {name!r} がありません。シート一覧: {names}")
    return workbook[name]


def copy_header_style(source, destination) -> None:
    """セル値を変えずに、通常のセル書式をすべてコピーする。"""
    destination.font = copy(source.font)
    destination.fill = copy(source.fill)
    destination.border = copy(source.border)
    destination.alignment = copy(source.alignment)
    destination.number_format = source.number_format
    destination.protection = copy(source.protection)


def resolve_column_dimension(worksheet, column_index: int) -> tuple[ColumnDimension, str]:
    """min～maxの範囲定義を展開し、指定列に実際に適用される定義を返す。

    Excelは、連続列をまとめて非表示にした場合などに、例えば
    ``<col min="44" max="47" hidden="1">`` のように1件の範囲として保存する。
    openpyxlではこの定義が先頭列のキーにだけ保持されるため、列記号による
    直接参照ではなく、すべてのColumnDimensionのmin～maxを調べる。
    """
    candidates: list[tuple[int, int, ColumnDimension]] = []
    for order, dim in enumerate(worksheet.column_dimensions.values()):
        dim.reindex()
        if dim.min <= column_index <= dim.max:
            # 重複定義があれば、より狭く具体的な範囲を優先する。
            candidates.append((dim.max - dim.min, -order, dim))

    if candidates:
        _, _, dim = min(candidates, key=lambda item: (item[0], item[1]))
        source_range = (
            get_column_letter(dim.min)
            if dim.min == dim.max
            else f"{get_column_letter(dim.min)}:{get_column_letter(dim.max)}"
        )
        return dim, source_range

    # 列定義がない場合はopenpyxlの既定ColumnDimensionを返す。
    return ColumnDimension(
        worksheet, index=get_column_letter(column_index)
    ), "既定値"


def copy_column_dimension(
    source_ws, source_col: int, target_ws, target_col: int
) -> tuple[ColumnDimension, str]:
    """範囲定義を解決して、列幅と表示属性を対象の1列へコピーする。"""
    src, source_range = resolve_column_dimension(source_ws, source_col)
    dst = target_ws.column_dimensions[get_column_letter(target_col)]
    dst.width = src.width
    dst.hidden = src.hidden
    dst.bestFit = src.bestFit
    dst.outlineLevel = src.outlineLevel
    dst.collapsed = src.collapsed
    return src, source_range


def color_text(color) -> str:
    """openpyxlのColorを、種類を失わない文字列にする。"""
    if color is None:
        return "なし"
    color_type = color.type
    if color_type == "rgb":
        return f"rgb:{color.rgb}"
    if color_type == "indexed":
        return f"indexed:{color.indexed}"
    if color_type == "theme":
        tint = f",tint={color.tint:g}" if color.tint else ""
        return f"theme:{color.theme}{tint}"
    if color_type == "auto":
        return f"auto:{color.auto}"
    return f"{color_type}:{color.value}"


def border_text(border) -> str:
    def side_text(side) -> str:
        if side is None or side.style is None:
            return "なし"
        return f"{side.style}/{color_text(side.color)}"

    return ",".join(
        f"{name}={side_text(getattr(border, attr))}"
        for name, attr in (
            ("左", "left"), ("右", "right"),
            ("上", "top"), ("下", "bottom"),
        )
    )


def log_reference_columns(logger: logging.Logger, worksheet, header_row: int) -> None:
    """参照シートから実際に読み取った列幅・ヘッダー書式を全列記録する。"""
    logger.info("=== 参照ブックから読み取った列情報 ===")
    logger.info(
        "シート=%r / ヘッダー行=%d / 最大列数=%d / ヘッダー行高=%r",
        worksheet.title, header_row, worksheet.max_column,
        worksheet.row_dimensions[header_row].height,
    )
    for col in range(1, worksheet.max_column + 1):
        letter = get_column_letter(col)
        cell = worksheet.cell(header_row, col)
        dim, dimension_range = resolve_column_dimension(worksheet, col)
        logger.info(
            "参照列=%d(%s) | ラベル=%r | 列定義範囲=%s | 幅=%r | 非表示=%s | "
            "style_id=%s | font=%r size=%r bold=%s italic=%s font色=%s | "
            "塗り=%s fgColor=%s bgColor=%s | 罫線=%s | "
            "配置=horizontal:%r vertical:%r wrap:%s rotation:%r | 表示形式=%r",
            col, letter, cell.value, dimension_range, dim.width, dim.hidden,
            cell.style_id, cell.font.name, cell.font.sz, cell.font.bold,
            cell.font.italic, color_text(cell.font.color),
            cell.fill.fill_type, color_text(cell.fill.fgColor),
            color_text(cell.fill.bgColor), border_text(cell.border),
            cell.alignment.horizontal, cell.alignment.vertical,
            cell.alignment.wrap_text, cell.alignment.text_rotation,
            cell.number_format,
        )


def apply_display(
    reference_ws, target_ws, header_row: int, logger: logging.Logger
) -> tuple[int, list[object]]:
    if header_row < 1:
        raise ValueError("--header-row は1以上を指定してください。")

    # 同名ラベルがある場合は、左から第1出現、第2出現…の順に対応させる。
    reference_columns: dict[object, list[int]] = defaultdict(list)
    for col in range(1, reference_ws.max_column + 1):
        label = reference_ws.cell(header_row, col).value
        if label is not None:
            reference_columns[label].append(col)

    target_occurrences: dict[object, int] = defaultdict(int)
    unmatched: list[object] = []
    matched = 0

    for target_col in range(1, target_ws.max_column + 1):
        target_cell = target_ws.cell(header_row, target_col)
        label = target_cell.value
        if label is None:
            continue

        occurrence = target_occurrences[label]
        target_occurrences[label] += 1
        candidates = reference_columns.get(label, [])
        if occurrence >= len(candidates):
            unmatched.append(label)
            logger.warning(
                "不一致 | 対象列=%d(%s) | ラベル=%r | 参照列なし",
                target_col, get_column_letter(target_col), label,
            )
            continue

        source_col = candidates[occurrence]
        source_cell = reference_ws.cell(header_row, source_col)
        copy_header_style(source_cell, target_cell)
        source_dim, source_range = copy_column_dimension(
            reference_ws, source_col, target_ws, target_col
        )
        logger.info(
            "反映 | 対象列=%d(%s) <- 参照列=%d(%s) | ラベル=%r | "
            "列定義範囲=%s | 幅=%r | 非表示=%s",
            target_col, get_column_letter(target_col),
            source_col, get_column_letter(source_col), label,
            source_range, source_dim.width, source_dim.hidden,
        )
        matched += 1

    # ヘッダー行の高さも「1行目の書式」の一部として反映する。
    src_row = reference_ws.row_dimensions[header_row]
    dst_row = target_ws.row_dimensions[header_row]
    dst_row.height = src_row.height
    dst_row.hidden = src_row.hidden
    dst_row.outlineLevel = src_row.outlineLevel
    dst_row.collapsed = src_row.collapsed

    return matched, unmatched


def main() -> int:
    args = parse_args()
    output = args.output or args.input.with_name(
        f"{args.input.stem}_display{args.input.suffix}"
    )
    log_path = args.log or output.with_name("apply_display.log")

    if not args.input.is_file():
        raise FileNotFoundError(f"入力ファイルがありません: {args.input}")
    if not args.reference.is_file():
        raise FileNotFoundError(f"参照ファイルがありません: {args.reference}")
    if output.resolve() == args.input.resolve():
        raise ValueError("入力ファイルと出力ファイルには別のパスを指定してください。")

    log_path.parent.mkdir(parents=True, exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(message)s",
        handlers=[logging.FileHandler(log_path, mode="w", encoding="utf-8")],
        force=True,
    )
    logger = logging.getLogger("apply_display")
    logger.info("参照ファイル=%s", args.reference.resolve())
    logger.info("入力ファイル=%s", args.input.resolve())
    logger.info("出力ファイル=%s", output.resolve())

    keep_vba_input = args.input.suffix.lower() == ".xlsm"
    keep_vba_reference = args.reference.suffix.lower() == ".xlsm"
    reference_wb = load_workbook(args.reference, keep_vba=keep_vba_reference)
    target_wb = load_workbook(args.input, keep_vba=keep_vba_input)

    reference_ws = select_sheet(reference_wb, args.reference_sheet, "参照元")
    target_ws = select_sheet(target_wb, args.input_sheet, "入力")
    logger.info("参照シート=%r / 対象シート=%r", reference_ws.title, target_ws.title)
    log_reference_columns(logger, reference_ws, args.header_row)
    logger.info("=== 対象ブックへの反映結果 ===")
    matched, unmatched = apply_display(
        reference_ws, target_ws, args.header_row, logger
    )

    output.parent.mkdir(parents=True, exist_ok=True)
    target_wb.save(output)
    logger.info("保存完了=%s", output.resolve())
    logger.info("書式を反映した列数=%d / 不一致列数=%d", matched, len(unmatched))
    print(f"出力: {output}")
    print(f"ログ: {log_path}")
    print(f"書式を反映した列数: {matched}")
    if unmatched:
        unique = list(dict.fromkeys(unmatched))
        print(
            f"参照元に一致列がないラベル: {len(unmatched)}列 "
            f"({', '.join(map(repr, unique))})",
            file=sys.stderr,
        )
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError) as exc:
        print(f"エラー: {exc}", file=sys.stderr)
        raise SystemExit(1)
