#!/usr/bin/env python3
from __future__ import annotations

import json
import sys
import textwrap
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Sequence

from PIL import Image, ImageDraw, ImageFont

FONT_REGULAR = Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
FONT_BOLD = Path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
DEFAULT_WIDTH = 800
MIN_WIDTH = 400
MAX_WIDTH = 2400
MAX_ROWS = 100
MAX_COLUMNS = 20
ROWS_PER_PAGE = 20
TITLE_FONT_SIZE = 16
CELL_FONT_SIZE = 14
PADDING_X = 14
PADDING_Y = 10
CELL_GAP = 1
OUTER_MARGIN = 24
TITLE_GAP = 16
LINE_SPACING = 4


@dataclass(frozen=True)
class Theme:
    background: str
    header: str
    row_even: str
    row_odd: str
    text: str
    border: str


THEMES = {
    "dark": Theme(
        background="#0d1117",
        header="#1a3a2a",
        row_even="#161b22",
        row_odd="#0d1117",
        text="#e6edf3",
        border="#30363d",
    ),
    "light": Theme(
        background="#ffffff",
        header="#238636",
        row_even="#f6f8fa",
        row_odd="#ffffff",
        text="#24292f",
        border="#d0d7de",
    ),
}


class ValidationError(ValueError):
    pass


def load_font(path: Path, size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    try:
        return ImageFont.truetype(str(path), size=size)
    except OSError:
        return ImageFont.load_default()


def read_payload() -> dict[str, Any]:
    raw = sys.stdin.read().strip()
    if not raw:
        raise ValidationError("Expected JSON payload on stdin")

    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValidationError(f"Invalid JSON: {exc}") from exc

    if not isinstance(payload, dict):
        raise ValidationError("Top-level JSON value must be an object")

    return payload


def validate_payload(payload: dict[str, Any]) -> dict[str, Any]:
    headers = payload.get("headers")
    rows = payload.get("rows")
    title = payload.get("title")
    width = payload.get("width", DEFAULT_WIDTH)
    theme = payload.get("theme", "dark")

    if not isinstance(headers, list) or not headers:
        raise ValidationError("'headers' must be a non-empty list")
    if len(headers) > MAX_COLUMNS:
        raise ValidationError(f"Too many columns: {len(headers)} > {MAX_COLUMNS}")
    if not all(isinstance(item, (str, int, float, bool)) or item is None for item in headers):
        raise ValidationError("All headers must be scalar values")

    if not isinstance(rows, list) or not rows:
        raise ValidationError("'rows' must be a non-empty list")
    if len(rows) > MAX_ROWS:
        raise ValidationError(f"Too many rows: {len(rows)} > {MAX_ROWS}")

    normalized_rows: list[list[str]] = []
    expected_len = len(headers)
    for index, row in enumerate(rows, start=1):
        if not isinstance(row, list):
            raise ValidationError(f"Row {index} must be a list")
        if len(row) != expected_len:
            raise ValidationError(
                f"Row {index} has {len(row)} cells, expected {expected_len}"
            )
        normalized_rows.append(["" if cell is None else str(cell) for cell in row])

    if title is not None and not isinstance(title, str):
        raise ValidationError("'title' must be a string if provided")

    if not isinstance(width, int):
        raise ValidationError("'width' must be an integer")
    width = max(MIN_WIDTH, min(MAX_WIDTH, width))

    if theme not in THEMES:
        raise ValidationError("'theme' must be 'dark' or 'light'")

    return {
        "headers": [str(item) for item in headers],
        "rows": normalized_rows,
        "title": title.strip() if isinstance(title, str) else None,
        "width": width,
        "theme": theme,
    }


def text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> int:
    bbox = draw.textbbox((0, 0), text, font=font)
    return bbox[2] - bbox[0]


def line_height(draw: ImageDraw.ImageDraw, font: ImageFont.ImageFont) -> int:
    bbox = draw.textbbox((0, 0), "Ag", font=font)
    return bbox[3] - bbox[1]


def wrap_cell(
    draw: ImageDraw.ImageDraw,
    text: str,
    font: ImageFont.ImageFont,
    max_width: int,
) -> list[str]:
    content = text.strip()
    if not content:
        return [""]

    paragraphs = content.splitlines() or [content]
    wrapped: list[str] = []

    for paragraph in paragraphs:
        words = paragraph.split()
        if not words:
            wrapped.append("")
            continue

        current = words[0]
        for word in words[1:]:
            candidate = f"{current} {word}"
            if text_width(draw, candidate, font) <= max_width:
                current = candidate
                continue

            wrapped.extend(split_long_token(draw, current, font, max_width))
            current = word

        wrapped.extend(split_long_token(draw, current, font, max_width))

    return wrapped or [""]


def split_long_token(
    draw: ImageDraw.ImageDraw,
    text: str,
    font: ImageFont.ImageFont,
    max_width: int,
) -> list[str]:
    if text_width(draw, text, font) <= max_width:
        return [text]

    chunks: list[str] = []
    current = ""
    for char in text:
        candidate = f"{current}{char}"
        if current and text_width(draw, candidate, font) > max_width:
            chunks.append(current)
            current = char
        else:
            current = candidate
    if current:
        chunks.append(current)
    return chunks or [text]


def compute_column_widths(
    total_width: int,
    headers: Sequence[str],
    rows: Sequence[Sequence[str]],
    draw: ImageDraw.ImageDraw,
    font: ImageFont.ImageFont,
    header_font: ImageFont.ImageFont,
) -> list[int]:
    content_width = total_width - (2 * OUTER_MARGIN)
    widths = [text_width(draw, header, header_font) + (2 * PADDING_X) for header in headers]

    sample_rows = rows[: min(len(rows), ROWS_PER_PAGE)]
    for row in sample_rows:
        for index, cell in enumerate(row):
            widths[index] = max(widths[index], text_width(draw, cell, font) + (2 * PADDING_X))

    total = sum(widths)
    if total <= content_width:
        slack = content_width - total
        if slack > 0:
            widths[-1] += slack
        return widths

    ratios = [max(width, 80) for width in widths]
    ratio_total = sum(ratios)
    scaled = [max(80, int(content_width * ratio / ratio_total)) for ratio in ratios]

    difference = content_width - sum(scaled)
    scaled[-1] += difference
    if scaled[-1] < 80:
        scaled[-1] = 80
        overflow = sum(scaled) - content_width
        for index in range(len(scaled) - 2, -1, -1):
            reducible = max(0, scaled[index] - 80)
            reduction = min(reducible, overflow)
            scaled[index] -= reduction
            overflow -= reduction
            if overflow == 0:
                break

    return scaled


def row_height(
    wrapped_cells: Sequence[Sequence[str]],
    draw: ImageDraw.ImageDraw,
    font: ImageFont.ImageFont,
) -> int:
    text_h = line_height(draw, font)
    max_lines = max(len(cell) for cell in wrapped_cells)
    content_h = (text_h * max_lines) + (LINE_SPACING * max(0, max_lines - 1))
    return content_h + (2 * PADDING_Y)


def draw_row(
    draw: ImageDraw.ImageDraw,
    top: int,
    widths: Sequence[int],
    wrapped_cells: Sequence[Sequence[str]],
    font: ImageFont.ImageFont,
    text_color: str,
    fill: str,
    border: str,
) -> int:
    height = row_height(wrapped_cells, draw, font)
    x = OUTER_MARGIN

    for width, lines in zip(widths, wrapped_cells):
        draw.rectangle(
            [(x, top), (x + width, top + height)],
            fill=fill,
            outline=border,
            width=1,
        )

        y = top + PADDING_Y
        for line in lines:
            draw.text((x + PADDING_X, y), line, fill=text_color, font=font)
            y += line_height(draw, font) + LINE_SPACING

        x += width - CELL_GAP

    return height


def render_table(payload: dict[str, Any]) -> Path:
    headers: list[str] = payload["headers"]
    rows: list[list[str]] = payload["rows"]
    title: str | None = payload["title"]
    width: int = payload["width"]
    theme = THEMES[payload["theme"]]

    title_font = load_font(FONT_BOLD, TITLE_FONT_SIZE)
    header_font = load_font(FONT_BOLD, CELL_FONT_SIZE)
    cell_font = load_font(FONT_REGULAR, CELL_FONT_SIZE)

    measure_image = Image.new("RGB", (width, 200), theme.background)
    measure_draw = ImageDraw.Draw(measure_image)
    column_widths = compute_column_widths(width, headers, rows, measure_draw, cell_font, header_font)

    row_pages = [rows[i : i + ROWS_PER_PAGE] for i in range(0, len(rows), ROWS_PER_PAGE)]
    title_block_height = 0
    if title:
        title_block_height = line_height(measure_draw, title_font) + TITLE_GAP

    header_wrapped = [
        wrap_cell(measure_draw, header, header_font, col_width - (2 * PADDING_X))
        for header, col_width in zip(headers, column_widths)
    ]
    header_height = row_height(header_wrapped, measure_draw, header_font)

    page_heights: list[int] = []
    wrapped_pages: list[list[list[list[str]]]] = []

    for page in row_pages:
        wrapped_rows = [
            [
                wrap_cell(measure_draw, cell, cell_font, col_width - (2 * PADDING_X))
                for cell, col_width in zip(row, column_widths)
            ]
            for row in page
        ]
        wrapped_pages.append(wrapped_rows)

        total_height = OUTER_MARGIN + title_block_height + header_height
        for wrapped in wrapped_rows:
            total_height += row_height(wrapped, measure_draw, cell_font) - CELL_GAP
        total_height += OUTER_MARGIN
        page_heights.append(total_height)

    total_height = sum(page_heights) + (OUTER_MARGIN if len(page_heights) > 1 else 0) * (len(page_heights) - 1)
    image = Image.new("RGB", (width, total_height), theme.background)
    draw = ImageDraw.Draw(image)

    current_top = 0
    for page_index, wrapped_rows in enumerate(wrapped_pages, start=1):
        y = current_top + OUTER_MARGIN

        page_title = title
        if title and len(wrapped_pages) > 1:
            page_title = f"{title} — часть {page_index}/{len(wrapped_pages)}"

        if page_title:
            draw.text((OUTER_MARGIN, y), page_title, fill=theme.text, font=title_font)
            y += line_height(draw, title_font) + TITLE_GAP

        draw_row(
            draw,
            y,
            column_widths,
            header_wrapped,
            header_font,
            theme.text,
            theme.header,
            theme.border,
        )
        y += header_height - CELL_GAP

        for row_index, wrapped in enumerate(wrapped_rows):
            fill = theme.row_even if row_index % 2 == 0 else theme.row_odd
            height = draw_row(
                draw,
                y,
                column_widths,
                wrapped,
                cell_font,
                theme.text,
                fill,
                theme.border,
            )
            y += height - CELL_GAP

        current_top += page_heights[page_index - 1]
        if page_index < len(wrapped_pages):
            current_top += OUTER_MARGIN

    output_path = Path("/tmp") / f"table_{datetime.utcnow().strftime('%Y%m%d%H%M%S%f')}.png"
    image.save(output_path, format="PNG")
    return output_path


def main() -> int:
    try:
        payload = validate_payload(read_payload())
        output_path = render_table(payload)
    except ValidationError as exc:
        print(str(exc), file=sys.stderr)
        return 1
    except Exception as exc:  # pragma: no cover - defensive CLI handling
        print(f"Unexpected error: {exc}", file=sys.stderr)
        return 1

    print(output_path)
    return 0


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