from __future__ import annotations

import argparse
import csv
import json
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path


DEFAULT_BASE_CSV = Path(
    r"C:\wamp64\www\atelier-com-site\racine\atelier-com-platform\outputs\pricing_import\pricing_import_20260526_100408.csv"
)
DEFAULT_OUTPUT_DIR = Path(
    r"C:\wamp64\www\atelier-com-site\racine\atelier-com-platform\outputs\pricing_import_corrections"
)

TARGET_SKUS = {
    "PAP-ACCROCHE-PORTE",
    "PAP-FLYERS",
    "PAP-CARTES-DE-VISITE",
    "OBJ-BOUTEILLES-ISOTHERMES-INOX",
}

ACCROCHE_QTYS = [25, 50, 100, 250, 500, 1000]
ACCROCHE_BASE_COSTS = {
    25: Decimal("68"),
    50: Decimal("68"),
    100: Decimal("79"),
    250: Decimal("123"),
    500: Decimal("171"),
    1000: Decimal("237"),
}
ACCROCHE_SUPPORT_FACTORS = {
    "250g/m² couché brillant": Decimal("1.00"),
    "300g/m² couché mat": Decimal("1.04"),
    "350g/m² couché mat": Decimal("1.08"),
}
ACCROCHE_IMPRESSION_FACTORS = {
    "Quadri recto": Decimal("0.89"),
    "quadri recto/verso": Decimal("1.00"),
}
ACCROCHE_FINISH_FACTORS = {
    "": Decimal("1.00"),
    "Mat": Decimal("1.10"),
    "Brillant": Decimal("1.10"),
}

FLYER_STANDARD_SUPPORT = {
    "135g": "Couché brillant",
    "170g": "Couché mat",
    "250g": "Couché brillant",
    "300g": "Couché mat",
}
FLYER_NO_FINISH_DIVISOR = Decimal("1.10")

CARDS_OPTIONAL_NONE = {"Aucune", "Aucun", "Sans", "None"}

BOTTLE_QTYS = [25, 50, 100, 250, 500, 1000]
BOTTLE_PRODUCT_UNITS = {
    25: Decimal("6.73"),
    50: Decimal("6.35"),
    100: Decimal("6.23"),
    250: Decimal("5.69"),
    500: Decimal("5.41"),
    1000: Decimal("5.16"),
}
BOTTLE_STANDARD_MARKING_UNITS = {
    25: Decimal("2.98"),
    50: Decimal("1.90"),
    100: Decimal("1.36"),
    250: Decimal("0.96"),
    500: Decimal("0.82"),
    1000: Decimal("0.74"),
}


@dataclass(frozen=True)
class RowKey:
    sku: str
    lot_quantity: str
    fields: tuple[tuple[str, str], ...]


def timestamp() -> str:
    return datetime.now().strftime("%Y%m%d_%H%M%S")


def trim(value: str | None) -> str:
    return (value or "").strip()


def parse_money(value: str | None) -> Decimal:
    raw = trim(value).replace(",", ".")
    return Decimal(raw) if raw else Decimal("0")


def quantize_integer(value: Decimal) -> Decimal:
    return value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)


def quantize_money(value: Decimal) -> Decimal:
    return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def format_money(value: Decimal) -> str:
    value = quantize_money(value)
    text = format(value, "f")
    if "." in text:
        text = text.rstrip("0").rstrip(".")
    return text


def load_rows(path: Path) -> tuple[list[str], list[dict[str, str]]]:
    with path.open("r", encoding="utf-8-sig", newline="") as fh:
        reader = csv.DictReader(fh, delimiter=";")
        return list(reader.fieldnames or []), list(reader)


def base_headers(headers: list[str]) -> list[str]:
    preferred = ["sku", "lot_quantity", "buy_price_ht"]
    remainder = [header for header in headers if header not in preferred]
    return preferred + remainder


def empty_row(headers: list[str]) -> dict[str, str]:
    return {header: "" for header in headers}


def normalize_none_option(value: str) -> str:
    return "" if trim(value) in CARDS_OPTIONAL_NONE else trim(value)


def row_identity(row: dict[str, str], headers: list[str]) -> RowKey:
    fields = tuple(
        (header, trim(row.get(header, "")))
        for header in headers
        if header not in {"sku", "lot_quantity", "buy_price_ht"} and trim(row.get(header, "")) != ""
    )
    return RowKey(trim(row.get("sku", "")), trim(row.get("lot_quantity", "")), fields)


def dedupe_rows(rows: list[dict[str, str]], headers: list[str]) -> list[dict[str, str]]:
    unique: dict[RowKey, dict[str, str]] = {}
    for row in rows:
        key = row_identity(row, headers)
        existing = unique.get(key)
        if existing is None:
            unique[key] = row
            continue
        existing_price = parse_money(existing.get("buy_price_ht"))
        next_price = parse_money(row.get("buy_price_ht"))
        if next_price < existing_price:
            unique[key] = row
    return list(unique.values())


def build_accroche_rows(headers: list[str], rows: list[dict[str, str]]) -> list[dict[str, str]]:
    combos: set[tuple[str, str, str, str]] = set()
    for row in rows:
        combos.add(
            (
                trim(row.get("format")),
                trim(row.get("support-matiere")),
                trim(row.get("impression")),
                normalize_none_option(row.get("finition")),
            )
        )

    output: list[dict[str, str]] = []
    for fmt, support, impression, finish in sorted(combos):
        support_factor = ACCROCHE_SUPPORT_FACTORS.get(support, Decimal("1.00"))
        impression_factor = ACCROCHE_IMPRESSION_FACTORS.get(impression, Decimal("1.00"))
        finish_factor = ACCROCHE_FINISH_FACTORS.get(finish, Decimal("1.00"))
        combo_factor = support_factor * impression_factor * finish_factor

        for qty in ACCROCHE_QTYS:
            row = empty_row(headers)
            row["sku"] = "PAP-ACCROCHE-PORTE"
            row["lot_quantity"] = str(qty)
            row["buy_price_ht"] = format_money(
                quantize_integer(ACCROCHE_BASE_COSTS[qty] * combo_factor)
            )
            row["format"] = fmt
            row["support-matiere"] = support
            row["impression"] = impression
            row["finition"] = finish
            row["decoupe"] = "A la forme"
            output.append(row)

    return output


def flyer_reference_maps(rows: list[dict[str, str]]) -> tuple[dict[tuple[str, str, str, str, str, str], Decimal], dict[tuple[str, str, str, str, str], Decimal]]:
    by_finish: dict[tuple[str, str, str, str, str, str], Decimal] = {}
    finish_floor: dict[tuple[str, str, str, str, str], Decimal] = {}

    for row in rows:
        fmt = trim(row.get("format"))
        support = trim(row.get("support-matiere"))
        gram = trim(row.get("grammage-epaisseur"))
        impression = trim(row.get("impression"))
        finish = normalize_none_option(row.get("finition"))
        qty = trim(row.get("lot_quantity"))
        price = parse_money(row.get("buy_price_ht"))
        if fmt == "" or support == "" or gram == "" or impression == "" or qty == "":
            continue

        key = (fmt, support, gram, impression, finish, qty)
        by_finish[key] = price

        floor_key = (fmt, support, gram, impression, qty)
        current_floor = finish_floor.get(floor_key)
        if current_floor is None or price < current_floor:
            finish_floor[floor_key] = price

    return by_finish, finish_floor


def flyer_price(
    by_finish: dict[tuple[str, str, str, str, str, str], Decimal],
    finish_floor: dict[tuple[str, str, str, str, str], Decimal],
    fmt: str,
    support_label: str,
    gram: str,
    impression: str,
    finish: str,
    qty: str,
) -> Decimal:
    standard_support = FLYER_STANDARD_SUPPORT.get(gram, support_label)

    if finish == "":
        floor = finish_floor.get((fmt, standard_support, gram, impression, qty))
        if floor is None:
            floor = finish_floor.get((fmt, support_label, gram, impression, qty), Decimal("0"))
        return quantize_integer(floor / FLYER_NO_FINISH_DIVISOR)

    exact = by_finish.get((fmt, standard_support, gram, impression, finish, qty))
    if exact is not None:
        return exact

    fallback = by_finish.get((fmt, support_label, gram, impression, finish, qty))
    if fallback is not None:
        return fallback

    floor = finish_floor.get((fmt, standard_support, gram, impression, qty))
    if floor is None:
        floor = finish_floor.get((fmt, support_label, gram, impression, qty), Decimal("0"))
    return floor


def build_flyer_rows(headers: list[str], rows: list[dict[str, str]]) -> list[dict[str, str]]:
    combos: set[tuple[str, str, str, str]] = set()
    for row in rows:
        combos.add(
            (
                trim(row.get("format")),
                trim(row.get("support-matiere")),
                trim(row.get("grammage-epaisseur")),
                trim(row.get("impression")),
            )
        )

    by_finish, finish_floor = flyer_reference_maps(rows)
    qtys = sorted({trim(row.get("lot_quantity")) for row in rows if trim(row.get("lot_quantity")) != ""}, key=lambda item: int(item))
    output: list[dict[str, str]] = []

    for fmt, support_label, gram, impression in sorted(combos):
        finishes = [""] if gram in {"135g", "170g"} else ["", "Brillant", "Mat"]
        for finish in finishes:
            for qty in qtys:
                row = empty_row(headers)
                row["sku"] = "PAP-FLYERS"
                row["lot_quantity"] = qty
                row["buy_price_ht"] = format_money(
                    flyer_price(by_finish, finish_floor, fmt, support_label, gram, impression, finish, qty)
                )
                row["format"] = fmt
                row["support-matiere"] = support_label
                row["grammage-epaisseur"] = gram
                row["impression"] = impression
                row["finition"] = finish
                output.append(row)

    return output


def build_cards_rows(headers: list[str], rows: list[dict[str, str]]) -> list[dict[str, str]]:
    output: list[dict[str, str]] = []
    for source in rows:
        row = empty_row(headers)
        for header in headers:
            row[header] = trim(source.get(header, ""))
        row["pelliculage"] = normalize_none_option(source.get("pelliculage"))
        row["finition"] = normalize_none_option(source.get("finition"))
        output.append(row)
    return output


def build_bottle_rows(headers: list[str]) -> list[dict[str, str]]:
    output: list[dict[str, str]] = []
    for qty in BOTTLE_QTYS:
        total = quantize_money(
            Decimal(qty) * (BOTTLE_PRODUCT_UNITS[qty] + BOTTLE_STANDARD_MARKING_UNITS[qty])
        )
        row = empty_row(headers)
        row["sku"] = "OBJ-BOUTEILLES-ISOTHERMES-INOX"
        row["lot_quantity"] = str(qty)
        row["buy_price_ht"] = format_money(total)
        row["format"] = "500 ml"
        output.append(row)
    return output


def build_patch_rows(headers: list[str], rows: list[dict[str, str]]) -> tuple[list[dict[str, str]], dict[str, int]]:
    rows_by_sku: dict[str, list[dict[str, str]]] = defaultdict(list)
    for row in rows:
        sku = trim(row.get("sku"))
        if sku in TARGET_SKUS:
            rows_by_sku[sku].append(row)

    patch_rows: list[dict[str, str]] = []
    patch_rows.extend(build_accroche_rows(headers, rows_by_sku["PAP-ACCROCHE-PORTE"]))
    patch_rows.extend(build_flyer_rows(headers, rows_by_sku["PAP-FLYERS"]))
    patch_rows.extend(build_cards_rows(headers, rows_by_sku["PAP-CARTES-DE-VISITE"]))
    patch_rows.extend(build_bottle_rows(headers))

    patch_rows = dedupe_rows(patch_rows, headers)
    patch_rows.sort(
        key=lambda row: (
            trim(row.get("sku")),
            tuple(trim(row.get(header, "")) for header in headers if header not in {"sku", "lot_quantity", "buy_price_ht"}),
            int(trim(row.get("lot_quantity")) or "0"),
        )
    )

    counts = defaultdict(int)
    for row in patch_rows:
        counts[trim(row.get("sku"))] += 1

    return patch_rows, dict(counts)


def build_full_rows(
    headers: list[str],
    rows: list[dict[str, str]],
    patch_rows: list[dict[str, str]],
) -> tuple[list[dict[str, str]], dict[str, int]]:
    preserved = [row for row in rows if trim(row.get("sku")) not in TARGET_SKUS]
    full_rows = preserved + patch_rows
    full_rows.sort(
        key=lambda row: (
            trim(row.get("sku")),
            tuple(trim(row.get(header, "")) for header in headers if header not in {"sku", "lot_quantity", "buy_price_ht"}),
            int(trim(row.get("lot_quantity")) or "0"),
        )
    )

    counts = defaultdict(int)
    for row in full_rows:
        counts[trim(row.get("sku"))] += 1

    return full_rows, dict(counts)


def write_csv(headers: list[str], rows: list[dict[str, str]], output_path: Path) -> None:
    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8-sig", newline="") as fh:
        writer = csv.DictWriter(fh, fieldnames=headers, delimiter=";")
        writer.writeheader()
        writer.writerows(rows)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base-csv", default=str(DEFAULT_BASE_CSV))
    parser.add_argument("--output-dir", default=str(DEFAULT_OUTPUT_DIR))
    parser.add_argument("--mode", choices=["patch", "full"], default="patch")
    args = parser.parse_args()

    base_csv = Path(args.base_csv)
    output_dir = Path(args.output_dir)

    headers, rows = load_rows(base_csv)
    headers = base_headers(headers)
    patch_rows, counts = build_patch_rows(headers, rows)
    final_rows = patch_rows
    final_counts = counts

    if args.mode == "full":
        final_rows, final_counts = build_full_rows(headers, rows, patch_rows)

    stamp = timestamp()
    stem = "pricing_import_correction_patch" if args.mode == "patch" else "pricing_import_corrected_full"
    output_csv = output_dir / f"{stem}_{stamp}.csv"
    output_summary = output_dir / f"{stem}_{stamp}_summary.json"

    write_csv(headers, final_rows, output_csv)

    summary = {
        "source_csv": str(base_csv),
        "output_csv": str(output_csv),
        "mode": args.mode,
        "rows_total": len(final_rows),
        "rows_by_sku": final_counts,
        "corrected_target_rows": len(patch_rows),
        "corrected_target_rows_by_sku": counts,
        "rules": {
            "PAP-ACCROCHE-PORTE": "recalage sur l'exemple utilisateur 250g brillant R/V sans finition, variation par grammage/impression/finition",
            "PAP-FLYERS": "135g/170g sans finition uniquement, ajout des lignes sans finition en 250g/300g, mapping support standard par grammage",
            "PAP-CARTES-DE-VISITE": "normalisation des options Aucune vers des champs vides pour les attributs optionnels",
            "OBJ-BOUTEILLES-ISOTHERMES-INOX": "grille standard avec personnalisation 1 couleur corps rotatif incluse",
        },
    }
    output_summary.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")

    print(json.dumps(summary, ensure_ascii=False))


if __name__ == "__main__":
    main()
