from __future__ import annotations

import csv
import hashlib
import io
import math
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from email.utils import parsedate_to_datetime
from typing import Any

YOUTUBE_DAILY_TOTALS = "youtube_daily_totals"
YOUTUBE_VIDEO_TABLE = "youtube_video_table"
YOUTUBE_CHART = "youtube_chart"
UNKNOWN = "unknown"


@dataclass
class ParsedCsv:
    detected_type: str
    rows: list[dict[str, Any]] = field(default_factory=list)
    total_row: dict[str, Any] | None = None
    warnings: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    date_range_start: date | None = None
    date_range_end: date | None = None
    raw_rows: list[dict[str, str]] = field(default_factory=list)

    @property
    def row_count(self) -> int:
        return len(self.raw_rows)


def file_hash(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()


def read_csv(content: bytes) -> tuple[list[str], list[dict[str, str]]]:
    text = content.decode("utf-8-sig", errors="replace")
    if not text.strip():
        return [], []
    reader = csv.DictReader(io.StringIO(text))
    headers = list(reader.fieldnames or [])
    rows: list[dict[str, str]] = []
    for row in reader:
        rows.append({str(k or ""): str(v or "") for k, v in row.items()})
    return headers, rows


def normalize_header(value: str) -> str:
    return (value or "").strip().lower().replace("\ufeff", "")


def detect_csv_type(headers: list[str]) -> str:
    normalized = {normalize_header(h) for h in headers}
    if normalized == {"datum", "aufrufe"}:
        return YOUTUBE_DAILY_TOTALS
    if {"videos", "videotitel", "veröffentlichungszeitpunkt des videos", "dauer"}.issubset(normalized):
        if "wiedergabezeit (stunden)" in normalized or "impressionen" in normalized:
            return YOUTUBE_VIDEO_TABLE
        return YOUTUBE_CHART
    if {"datum", "videos", "videotitel", "veröffentlichungszeitpunkt des videos", "dauer", "aufrufe"}.issubset(normalized):
        return YOUTUBE_CHART
    return UNKNOWN


def parse_number(value: Any) -> float | None:
    if value is None:
        return None
    text = str(value).strip().replace("\u202f", "").replace(" ", "")
    if text in {"", "-", "—"}:
        return None
    text = text.replace("%", "")
    # German thousands/decimal support without breaking YouTube exports that use decimal points.
    if "," in text and "." in text:
        text = text.replace(".", "").replace(",", ".")
    elif "," in text:
        text = text.replace(",", ".")
    try:
        value_float = float(text)
    except ValueError:
        return None
    if math.isnan(value_float):
        return None
    return value_float


def parse_int(value: Any) -> int | None:
    number = parse_number(value)
    return int(number) if number is not None else None


def parse_percent(value: Any) -> float | None:
    return parse_number(value)


def parse_duration_seconds(value: Any) -> int | None:
    text = str(value or "").strip()
    if not text:
        return None
    parts = text.split(":")
    try:
        nums = [int(float(p)) for p in parts]
    except ValueError:
        return None
    if len(nums) == 1:
        return nums[0]
    if len(nums) == 2:
        return nums[0] * 60 + nums[1]
    if len(nums) == 3:
        return nums[0] * 3600 + nums[1] * 60 + nums[2]
    return None


def parse_youtube_date(value: Any) -> date | None:
    text = str(value or "").strip()
    if not text:
        return None
    for fmt in ("%Y-%m-%d", "%b %d, %Y", "%B %d, %Y", "%d.%m.%Y", "%d/%m/%Y"):
        try:
            return datetime.strptime(text, fmt).date()
        except ValueError:
            pass
    try:
        return parsedate_to_datetime(text).date()
    except Exception:
        return None


def get(row: dict[str, str], key: str) -> str:
    wanted = normalize_header(key)
    for k, v in row.items():
        if normalize_header(k) == wanted:
            return v
    return ""


def parse_youtube_daily_totals_csv(content: bytes) -> ParsedCsv:
    headers, raw_rows = read_csv(content)
    parsed = ParsedCsv(detected_type=YOUTUBE_DAILY_TOTALS, raw_rows=raw_rows)
    for index, row in enumerate(raw_rows, start=2):
        metric_date = parse_youtube_date(get(row, "Datum"))
        views = parse_int(get(row, "Aufrufe"))
        if metric_date is None:
            parsed.warnings.append(f"Row {index}: missing/invalid date")
            continue
        if views is None:
            parsed.warnings.append(f"Row {index}: missing views")
        parsed.rows.append({"metric_date": metric_date, "views": views, "raw": row})
    dates = [r["metric_date"] for r in parsed.rows if r.get("metric_date")]
    if dates:
        parsed.date_range_start = min(dates)
        parsed.date_range_end = max(dates)
    if not raw_rows:
        parsed.warnings.append("Header-only or empty daily totals CSV")
    return parsed


def is_total_row(row: dict[str, str]) -> bool:
    video_id = get(row, "Videos").strip().lower()
    title = get(row, "Videotitel").strip().lower()
    return video_id in {"", "gesamt", "total"} or title in {"gesamt", "total"}


def parse_youtube_video_table_csv(content: bytes) -> ParsedCsv:
    headers, raw_rows = read_csv(content)
    parsed = ParsedCsv(detected_type=YOUTUBE_VIDEO_TABLE, raw_rows=raw_rows)
    for index, row in enumerate(raw_rows, start=2):
        payload = {
            "external_post_id": get(row, "Videos").strip(),
            "title": get(row, "Videotitel").strip(),
            "published_at": parse_youtube_date(get(row, "Veröffentlichungszeitpunkt des Videos")),
            "duration_sec": parse_duration_seconds(get(row, "Dauer")),
            "views": parse_int(get(row, "Aufrufe")),
            "watch_time_hours": parse_number(get(row, "Wiedergabezeit (Stunden)")),
            "subscribers_delta": parse_int(get(row, "Abonnenten")),
            "impressions": parse_int(get(row, "Impressionen")),
            "impression_ctr_pct": parse_percent(get(row, "Klickrate der Impressionen (%)")),
            "raw": row,
            "row_number": index,
        }
        if is_total_row(row):
            parsed.total_row = payload
            continue
        if not payload["external_post_id"] and not payload["title"]:
            parsed.warnings.append(f"Row {index}: skipped empty video row")
            continue
        if payload["views"] is None:
            parsed.warnings.append(f"Row {index}: partial snapshot without views for {payload['external_post_id'] or payload['title']}")
        parsed.rows.append(payload)
    dates = [r["published_at"] for r in parsed.rows if r.get("published_at")]
    if dates:
        parsed.date_range_start = min(dates)
        parsed.date_range_end = max(dates)
    if not raw_rows:
        parsed.warnings.append("Header-only or empty video table CSV")
    return parsed


def parse_youtube_chart_csv(content: bytes) -> ParsedCsv:
    headers, raw_rows = read_csv(content)
    parsed = ParsedCsv(detected_type=YOUTUBE_CHART, raw_rows=raw_rows)
    for index, row in enumerate(raw_rows, start=2):
        metric_date = parse_youtube_date(get(row, "Datum"))
        parsed.rows.append({
            "metric_date": metric_date,
            "external_post_id": get(row, "Videos").strip(),
            "title": get(row, "Videotitel").strip(),
            "published_at": parse_youtube_date(get(row, "Veröffentlichungszeitpunkt des Videos")),
            "duration_sec": parse_duration_seconds(get(row, "Dauer")),
            "views": parse_int(get(row, "Aufrufe")),
            "raw": row,
            "row_number": index,
        })
    if not raw_rows:
        parsed.warnings.append("Header-only chart CSV accepted")
    dates = [r["metric_date"] for r in parsed.rows if r.get("metric_date")]
    if dates:
        parsed.date_range_start = min(dates)
        parsed.date_range_end = max(dates)
    return parsed


def parse_csv_by_type(content: bytes) -> ParsedCsv:
    headers, rows = read_csv(content)
    detected = detect_csv_type(headers)
    if detected == YOUTUBE_DAILY_TOTALS:
        return parse_youtube_daily_totals_csv(content)
    if detected == YOUTUBE_VIDEO_TABLE:
        return parse_youtube_video_table_csv(content)
    if detected == YOUTUBE_CHART:
        return parse_youtube_chart_csv(content)
    parsed = ParsedCsv(detected_type=UNKNOWN, raw_rows=rows)
    parsed.errors.append(f"Unknown CSV header: {headers}")
    return parsed


def compute_snapshot_metrics(row: dict[str, Any], snapshot_at: datetime) -> dict[str, Any]:
    views = row.get("views")
    watch_time = row.get("watch_time_hours")
    impressions = row.get("impressions")
    subscribers = row.get("subscribers_delta")
    duration = row.get("duration_sec")
    published_at = row.get("published_at")
    avg_view_duration_sec = None
    retention_proxy_pct = None
    views_per_impression = None
    subscribers_per_1000_views = None
    views_per_day_since_publish = None
    if views and watch_time is not None:
        avg_view_duration_sec = (watch_time * 3600) / views
        if duration:
            retention_proxy_pct = min(100.0, (avg_view_duration_sec / duration) * 100)
    if views is not None and impressions:
        views_per_impression = views / impressions
    if subscribers is not None and views:
        subscribers_per_1000_views = subscribers / views * 1000
    if views is not None and published_at:
        days = max(1, (snapshot_at.date() - published_at).days + 1)
        views_per_day_since_publish = views / days
    return {
        "avg_view_duration_sec": avg_view_duration_sec,
        "retention_proxy_pct": retention_proxy_pct,
        "views_per_impression": views_per_impression,
        "subscribers_per_1000_views": subscribers_per_1000_views,
        "views_per_day_since_publish": views_per_day_since_publish,
    }


def utcnow() -> datetime:
    return datetime.now(timezone.utc)
