#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Генератор смет студии фото- и видеосъёмки «Кадр».

Считает стоимость по трём позициям и собирает аккуратную HTML-смету:
  • съёмка    — 3 000 ₽ за час;
  • монтаж    — 2 000 ₽ за ролик;
  • выезд     — 1 000 ₽ за заказ (одной строкой, если выезд нужен).

НДС, скидки, наценки и прочие условия не добавляются никогда — считаем ровно
то, что задано прайсом. Отрицательные и нечисловые количества не принимаются:
программа останавливается с понятной ошибкой и ничего не записывает.

Использование (из командной строки):

    python3 kadr_smeta.py --client 'Кафе «Маяк»' --hours 2 --videos 3 --travel yes

  --client  имя клиента (обязательно)
  --hours   часы съёмки, неотрицательное число (обязательно)
  --videos  число роликов на монтаж, неотрицательное целое (обязательно)
  --travel  выезд: yes / no (по умолчанию yes)
  --date    дата в смете, ГГГГ-ММ-ДД (по умолчанию — сегодняшняя)
  --out     путь к HTML-файлу (по умолчанию smeta-<клиент>.html)
  --json    дополнительно вывести расчёт в JSON (для других программ)

Как библиотека:

    from kadr_smeta import build_estimate, render_html
    est = build_estimate("Кафе «Маяк»", hours=2, videos=3, travel=True)
    open("smeta.html", "w", encoding="utf-8").write(render_html(est))
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import date
from html import escape
from pathlib import Path

# ---------------------------------------------------------------- прайс студии
PRICE_PER_HOUR = 3000   # ₽ за час съёмки
PRICE_PER_VIDEO = 2000  # ₽ за монтаж одного ролика
PRICE_TRAVEL = 1000     # ₽ за выезд, за заказ

STUDIO = "Студия фото- и видеосъёмки «Кадр»"
NOTE = "Учебный пример — вымышленная студия."


# ------------------------------------------------------------------- служебное
def plural(n: float, one: str, few: str, many: str) -> str:
    """Русское склонение: 1 час, 2 часа, 5 часов."""
    n_int = int(abs(n))
    if abs(n - n_int) > 0:  # 1,5 — всегда «часа»
        return few
    if n_int % 10 == 1 and n_int % 100 != 11:
        return one
    if 2 <= n_int % 10 <= 4 and not 12 <= n_int % 100 <= 14:
        return few
    return many


def money(value: float) -> str:
    """13 000 ₽ — сумма с пробелом между тысячами, без копеек."""
    if float(value).is_integer():
        text = f"{int(round(value)):,}".replace(",", "\u00a0")
    else:
        text = f"{value:,.2f}".replace(",", "\u00a0").replace(".", ",")
    return f"{text}\u00a0₽"


def qty_text(value: float, one: str, few: str, many: str) -> str:
    if float(value).is_integer():
        return f"{int(round(value))} {plural(value, one, few, many)}"
    return f"{value:g}".replace(".", ",") + " " + plural(value, one, few, many)


# ------------------------------------------------------------------ проверки
class InputError(ValueError):
    """Некорректный ввод: отрицательное или нечисловое количество."""


def parse_hours(raw) -> float:
    """Часы съёмки: неотрицательное число."""
    try:
        value = float(str(raw).replace(",", ".").strip())
    except (TypeError, ValueError):
        raise InputError(f"Часы съёмки должны быть числом, а не «{raw}»")
    if value != value or value in (float("inf"), float("-inf")):
        raise InputError(f"Часы съёмки должны быть числом, а не «{raw}»")
    if value < 0:
        raise InputError(
            f"Отрицательное количество не принимается: часы съёмки = {value:g}"
        )
    return value


def parse_videos(raw) -> int:
    """Число роликов: неотрицательное целое."""
    text = str(raw).replace(",", ".").strip()
    if not re.fullmatch(r"[+-]?\d+(\.0+)?", text):
        raise InputError(f"Число роликов должно быть целым, а не «{raw}»")
    value = float(text)
    if value < 0:
        raise InputError(
            f"Отрицательное количество не принимается: роликов = {int(value)}"
        )
    return int(value)


def parse_travel(raw) -> bool:
    """Выезд: да/нет, yes/no, true/false, 1/0."""
    text = str(raw).strip().lower()
    if text in ("да", "yes", "y", "true", "1", "нужен", "нужно"):
        return True
    if text in ("нет", "no", "n", "false", "0", "не нужен", "не нужно"):
        return False
    raise InputError(f"Про выезд непонятно: «{raw}» — ответьте да или нет")


# ------------------------------------------------------------------- расчёт
def build_estimate(client: str, hours, videos, travel: bool = True, day=None) -> dict:
    """Считает смету. Возвращает словарь с позициями и итогом.

    Цены и правила — только из прайса студии: НДС, скидок и прочих строк нет.
    """
    client = str(client).strip()
    if not client:
        raise InputError("Имя клиента не может быть пустым")

    hours = parse_hours(hours)
    videos = parse_videos(videos)
    travel = parse_travel(travel) if not isinstance(travel, bool) else travel

    rows = []
    if hours > 0:
        rows.append(
            {
                "title": "Съёмка",
                "detail": qty_text(hours, "час", "часа", "часов")
                + " × "
                + money(PRICE_PER_HOUR)
                + " за час",
                "qty": hours,
                "unit": "час",
                "unit_forms": ("час", "часа", "часов"),
                "price": PRICE_PER_HOUR,
                "amount": hours * PRICE_PER_HOUR,
            }
        )
    if videos > 0:
        rows.append(
            {
                "title": "Монтаж",
                "detail": qty_text(videos, "ролик", "ролика", "роликов")
                + " × "
                + money(PRICE_PER_VIDEO)
                + " за ролик",
                "qty": videos,
                "unit": "ролик",
                "unit_forms": ("ролик", "ролика", "роликов"),
                "price": PRICE_PER_VIDEO,
                "amount": videos * PRICE_PER_VIDEO,
            }
        )
    if travel:
        rows.append(
            {
                "title": "Выезд",
                "detail": "за заказ, одним платежом",
                "qty": 1,
                "unit": "заказ",
                "unit_forms": ("заказ", "заказа", "заказов"),
                "price": PRICE_TRAVEL,
                "amount": PRICE_TRAVEL,
            }
        )

    total = sum(row["amount"] for row in rows)
    if day is None:
        day = date.today()
    elif isinstance(day, str):
        day = date.fromisoformat(day)

    return {
        "client": client,
        "date": day.isoformat(),
        "rows": rows,
        "total": total,
        "travel": travel,
        "hours": hours,
        "videos": videos,
    }


# --------------------------------------------------------------------- вывод
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Смета · {client}</title>
<style>
  :root {{
    --ink: #1b1b1f;
    --muted: #6d6d76;
    --line: #e3e3e8;
    --accent: #b8873f;
  }}
  * {{ box-sizing: border-box; }}
  body {{
    margin: 0; padding: 40px 18px 64px;
    background: #f6f6f7; color: var(--ink);
    font: 16px/1.5 -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
  }}
  .sheet {{
    max-width: 720px; margin: 0 auto; background: #fff;
    border: 1px solid var(--line); border-radius: 14px;
    padding: 40px 44px 32px; box-shadow: 0 12px 34px rgba(20,20,30,.07);
  }}
  .top {{ display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap;
          border-bottom: 2px solid var(--ink); padding-bottom: 18px; }}
  .studio {{ font-size: 19px; font-weight: 700; letter-spacing: .2px; }}
  .studio span {{ display: block; font-size: 13px; font-weight: 400; color: var(--muted); margin-top: 4px; }}
  .doc {{ text-align: right; }}
  .doc b {{ font-size: 13px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); }}
  .doc em {{ display: block; font-style: normal; font-size: 13px; color: var(--muted); margin-top: 4px; }}
  h1 {{ font-size: 24px; margin: 26px 0 4px; }}
  .client {{ color: var(--muted); margin: 0 0 24px; font-size: 15px; }}
  table {{ width: 100%; border-collapse: collapse; }}
  th, td {{ padding: 12px 8px; text-align: left; vertical-align: top; }}
  th {{ font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: var(--muted);
        border-bottom: 1px solid var(--line); font-weight: 600; }}
  tbody tr + tr td {{ border-top: 1px solid var(--line); }}
  .num {{ text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }}
  .item b {{ font-weight: 600; }}
  .item span {{ display: block; font-size: 13px; color: var(--muted); margin-top: 2px; }}
  tfoot td {{ border-top: 2px solid var(--ink); padding-top: 16px; font-size: 20px; font-weight: 700; }}
  .terms {{ margin-top: 26px; border-top: 1px solid var(--line); padding-top: 16px;
            font-size: 13px; color: var(--muted); }}
  .terms b {{ color: var(--ink); }}
  .foot {{ margin-top: 22px; font-size: 12px; color: #9a9aa2; text-align: center; }}
  .empty {{ padding: 18px 0; color: var(--muted); }}
  @media print {{
    body {{ background: #fff; padding: 0; }}
    .sheet {{ border: 0; box-shadow: none; border-radius: 0; max-width: none; padding: 0; }}
  }}
  @media (max-width: 560px) {{
    .sheet {{ padding: 26px 18px; }}
    .doc {{ text-align: left; }}
  }}
</style>
</head>
<body>
  <div class="sheet">
    <div class="top">
      <div class="studio">Студия «Кадр»<span>фото- и видеосъёмка · монтаж</span></div>
      <div class="doc"><b>Смета</b><em>от {date_ru}</em></div>
    </div>

    <h1>Смета на съёмочные работы</h1>
    <p class="client">Клиент: <b>{client}</b></p>

    {table}

    <div class="terms">
      <b>Условия расчёта:</b> цена часа съёмки — {p_hour}, монтаж одного ролика — {p_video},
      выезд — {p_travel} за заказ. В смете только эти позиции: НДС, скидки и наценки не начисляются.
    </div>

    <p class="foot">{note}</p>
  </div>
</body>
</html>
"""


def _table(est: dict) -> str:
    if not est["rows"]:
        return (
            '<p class="empty">Позиции не выбраны: укажите часы съёмки или число роликов — '
            "смета не может состоять только из выезда.</p>"
        )
    body = "\n".join(
        '        <tr>\n'
        f'          <td class="item"><b>{escape(row["title"])}</b>'
        f'<span>{escape(row["detail"])}</span></td>\n'
        f'          <td class="num">{escape(qty_text(row["qty"], *row["unit_forms"]))}</td>\n'
        f'          <td class="num">{escape(money(row["price"]))}</td>\n'
        f'          <td class="num">{escape(money(row["amount"]))}</td>\n'
        "        </tr>"
        for row in est["rows"]
    )
    return (
        "    <table>\n"
        "      <thead>\n"
        "        <tr><th>Позиция</th><th class=\"num\">Количество</th>"
        "<th class=\"num\">Цена</th><th class=\"num\">Сумма</th></tr>\n"
        "      </thead>\n"
        "      <tbody>\n"
        f"{body}\n"
        "      </tbody>\n"
        "      <tfoot>\n"
        f'        <tr><td colspan="3">Итого</td>'
        f'<td class="num">{escape(money(est["total"]))}</td></tr>\n'
        "      </tfoot>\n"
        "    </table>"
    )


RU_MONTHS = (
    "января", "февраля", "марта", "апреля", "мая", "июня",
    "июля", "августа", "сентября", "октября", "ноября", "декабря",
)


def render_html(est: dict) -> str:
    """Собирает готовую HTML-смету (один файл, без внешних зависимостей)."""
    day = date.fromisoformat(est["date"])
    return HTML_TEMPLATE.format(
        client=escape(est["client"]),
        date_ru=f"{day.day} {RU_MONTHS[day.month - 1]} {day.year}",
        table=_table(est),
        p_hour=money(PRICE_PER_HOUR),
        p_video=money(PRICE_PER_VIDEO),
        p_travel=money(PRICE_TRAVEL),
        note=NOTE,
    )


def slug(text: str) -> str:
    """«Кафе «Маяк»» → «kafe-mayak» (без экзотики в имени файла)."""
    table = str.maketrans(
        "абвгдеёжзийклмнопрстуфхцчшщъыьэюя",
        "abvgdeejzijklmnoprstufhccss_y_eua",
    )
    text = text.lower().translate(table)
    text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
    return text or "smeta"


def save_html(est: dict, path) -> Path:
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(render_html(est), encoding="utf-8")
    return path


# ---------------------------------------------------------------------- CLI
def main(argv=None) -> int:
    parser = argparse.ArgumentParser(
        description="Генератор смет студии «Кадр»: съёмка 3000 ₽/час, монтаж 2000 ₽/ролик, выезд 1000 ₽/заказ."
    )
    parser.add_argument("--client", required=True, help="имя клиента")
    parser.add_argument("--hours", required=True, help="часы съёмки (неотрицательное число)")
    parser.add_argument("--videos", required=True, help="число роликов на монтаж (целое, ≥ 0)")
    parser.add_argument("--travel", default="yes", help="выезд: yes / no (по умолчанию yes)")
    parser.add_argument("--date", default=None, help="дата сметы, ГГГГ-ММ-ДД (по умолчанию сегодня)")
    parser.add_argument("--out", default=None, help="путь к HTML-файлу")
    parser.add_argument("--json", action="store_true", help="вывести расчёт в JSON")
    args = parser.parse_args(argv)

    try:
        est = build_estimate(
            client=args.client,
            hours=args.hours,
            videos=args.videos,
            travel=args.travel,
            day=args.date,
        )
    except InputError as err:
        print(f"Ошибка: {err}", file=sys.stderr)
        return 2

    out = Path(args.out) if args.out else Path(f"smeta-{slug(est['client'])}.html")
    save_html(est, out)

    print(f"Смета: {out}")
    for row in est["rows"]:
        print(f"  {row['title']:<8} {qty_text(row['qty'], *row['unit_forms'])}: {money(row['amount'])}")
    print(f"  Итого: {money(est['total'])}")

    if args.json:
        print(json.dumps(est, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
