"""GA4 Analytics helpers for seoreport."""

from __future__ import annotations

import datetime as dt
import os
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Optional

from google.auth.transport.requests import AuthorizedSession, Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

GA_SCOPES = [
    "https://www.googleapis.com/auth/webmasters.readonly",
    "https://www.googleapis.com/auth/analytics.readonly",
]
GA_REPORT_ENDPOINT = "https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport"
GA_METADATA_ENDPOINT = "https://analyticsdata.googleapis.com/v1beta/properties/{property_id}/metadata"

METRIC_KEY_MAP = {
    "sessions": "sessions",
    "totalUsers": "users",
    "engagedSessions": "engaged_sessions",
    "engagementRate": "engagement_rate",
    "averageSessionDuration": "avg_session_duration",
    "screenPageViews": "views",
}

INT_METRICS = {"sessions", "totalUsers", "engagedSessions", "screenPageViews"}


@dataclass
class GaOptions:
    property_id: str
    client_secrets: str
    token: str
    port: int
    direct_share: float
    source_regex: str
    channel_group_name: str
    channel_group_id: Optional[str] = None


def load_ga_credentials(client_secrets_path: str, token_path: str, port: int) -> Credentials:
    creds = None
    if os.path.exists(token_path):
        try:
            creds = Credentials.from_authorized_user_file(token_path, GA_SCOPES)
        except Exception:
            creds = None

    if creds and creds.scopes:
        missing = set(GA_SCOPES) - set(creds.scopes)
        if missing:
            print(
                "Warning: GA OAuth token is missing required scopes. "
                "Re-auth is required to update scopes."
            )
            creds = None

    if creds and creds.expired and creds.refresh_token:
        creds.refresh(Request())
    elif not creds or not creds.valid:
        flow = InstalledAppFlow.from_client_secrets_file(client_secrets_path, GA_SCOPES)
        creds = flow.run_local_server(
            port=port,
            access_type="offline",
            prompt="consent",
            include_granted_scopes="true",
        )

    with open(token_path, "w", encoding="utf-8") as f:
        f.write(creds.to_json())

    return creds


def ga_post(session: AuthorizedSession, property_id: str, payload: dict, timeout: int = 60) -> dict:
    url = GA_REPORT_ENDPOINT.format(property_id=property_id)
    response = session.post(url, json=payload, timeout=timeout)
    if response.status_code != 200:
        raise RuntimeError(
            f"GA4 API request failed ({response.status_code}): {response.text}"
        )
    return response.json()


def ga_get_metadata(session: AuthorizedSession, property_id: str, timeout: int = 60) -> dict:
    url = GA_METADATA_ENDPOINT.format(property_id=property_id)
    response = session.get(url, timeout=timeout)
    if response.status_code != 200:
        raise RuntimeError(
            f"GA4 Metadata request failed ({response.status_code}): {response.text}"
        )
    return response.json()


def _build_report_payload(
    start: dt.date,
    end: dt.date,
    metrics: Iterable[str],
    dimensions: Optional[Iterable[str]] = None,
    dimension_filter: Optional[dict] = None,
    keep_empty_rows: bool = False,
    order_by_dimension: Optional[str] = None,
) -> dict:
    payload: Dict[str, Any] = {
        "dateRanges": [{"startDate": start.isoformat(), "endDate": end.isoformat()}],
        "metrics": [{"name": name} for name in metrics],
    }
    if dimensions:
        payload["dimensions"] = [{"name": name} for name in dimensions]
    if dimension_filter:
        payload["dimensionFilter"] = dimension_filter
    if keep_empty_rows:
        payload["keepEmptyRows"] = True
    if order_by_dimension:
        payload["orderBys"] = [{"dimension": {"dimensionName": order_by_dimension}}]
    return payload


def _string_filter(field_name: str, value: str, match_type: str) -> dict:
    return {
        "filter": {
            "fieldName": field_name,
            "stringFilter": {
                "matchType": match_type,
                "value": value,
                "caseSensitive": False,
            },
        }
    }


def _or_filter(filters: List[dict]) -> dict:
    return {"orGroup": {"expressions": filters}}


def _starts_with(field_name: str, value: str) -> dict:
    return _string_filter(field_name, value, "BEGINS_WITH")


def _parse_metrics_row(row: dict, metrics: List[str]) -> Dict[str, float]:
    values = row.get("metricValues", [])
    parsed: Dict[str, float] = {}
    for idx, metric in enumerate(metrics):
        raw = values[idx].get("value", "0") if idx < len(values) else "0"
        try:
            val = float(raw)
        except ValueError:
            val = 0.0
        if metric in INT_METRICS:
            parsed[metric] = float(int(round(val)))
        else:
            parsed[metric] = val
    return parsed


def _parse_single_totals(response: dict, metrics: List[str]) -> Dict[str, float]:
    rows = response.get("rows", [])
    if not rows:
        return {metric: 0.0 for metric in metrics}
    return _parse_metrics_row(rows[0], metrics)


def _parse_daily(response: dict, metrics: List[str]) -> Dict[str, Dict[str, float]]:
    rows = response.get("rows", [])
    daily: Dict[str, Dict[str, float]] = {}
    for row in rows:
        dim_vals = row.get("dimensionValues", [])
        if not dim_vals:
            continue
        date_val = dim_vals[0].get("value")
        if not date_val:
            continue
        daily[date_val] = _parse_metrics_row(row, metrics)
    return daily


def _normalize_metrics(raw: Dict[str, float]) -> Dict[str, float]:
    return {
        "sessions": raw.get("sessions", 0.0),
        "users": raw.get("totalUsers", 0.0),
        "engaged_sessions": raw.get("engagedSessions", 0.0),
        "views": raw.get("screenPageViews", 0.0),
        "avg_session_duration": raw.get("averageSessionDuration", 0.0),
    }


def _add_share(base: Dict[str, float], direct: Dict[str, float], share: float) -> Dict[str, float]:
    return {
        "sessions": base["sessions"] + direct["sessions"] * share,
        "users": base["users"] + direct["users"] * share,
        "engaged_sessions": base["engaged_sessions"] + direct["engaged_sessions"] * share,
        "views": base["views"] + direct["views"] * share,
        "total_duration": base["total_duration"] + direct["total_duration"] * share,
    }


def _subtract_metrics(total: Dict[str, float], part: Dict[str, float]) -> Dict[str, float]:
    return {
        "sessions": total["sessions"] - part["sessions"],
        "users": total["users"] - part["users"],
        "engaged_sessions": total["engaged_sessions"] - part["engaged_sessions"],
        "views": total["views"] - part["views"],
        "total_duration": total["total_duration"] - part["total_duration"],
    }


def _finalize(metrics: Dict[str, float]) -> Dict[str, float]:
    sessions = metrics["sessions"]
    engaged_sessions = metrics["engaged_sessions"]
    avg_session_duration = metrics["total_duration"] / sessions if sessions > 0 else 0.0
    engagement_rate = engaged_sessions / sessions if sessions > 0 else 0.0
    return {
        "sessions": metrics["sessions"],
        "users": metrics["users"],
        "engaged_sessions": metrics["engaged_sessions"],
        "views": metrics["views"],
        "avg_session_duration": avg_session_duration,
        "engagement_rate": engagement_rate,
    }


def allocate_direct_share(
    total_raw: Dict[str, float],
    ai_raw: Dict[str, float],
    direct_raw: Dict[str, float],
    direct_share: float,
) -> Dict[str, Dict[str, float]]:
    total = _normalize_metrics(total_raw)
    ai = _normalize_metrics(ai_raw)
    direct = _normalize_metrics(direct_raw)

    total_duration = total["avg_session_duration"] * total["sessions"]
    ai_duration = ai["avg_session_duration"] * ai["sessions"]
    direct_duration = direct["avg_session_duration"] * direct["sessions"]

    total_with_duration = {**total, "total_duration": total_duration}
    ai_with_duration = {**ai, "total_duration": ai_duration}
    direct_with_duration = {**direct, "total_duration": direct_duration}

    ai_adj = _add_share(ai_with_duration, direct_with_duration, direct_share)
    direct_adj = _add_share({k: 0.0 for k in ai_adj}, direct_with_duration, 1 - direct_share)
    non_ai = _subtract_metrics(total_with_duration, ai_adj)

    return {
        "total": _finalize(total_with_duration),
        "ai": _finalize(ai_adj),
        "direct": _finalize(direct_adj),
        "web": _finalize(non_ai),
    }


def resolve_channel_group_api_name(
    session: AuthorizedSession,
    property_id: str,
    channel_group_name: str,
    channel_group_id: Optional[str],
) -> Optional[str]:
    if channel_group_id:
        return f"sessionCustomChannelGroup:{channel_group_id}"

    metadata = ga_get_metadata(session, property_id)
    for dimension in metadata.get("dimensions", []):
        if dimension.get("uiName") == channel_group_name:
            api_name = dimension.get("apiName")
            if api_name and api_name.startswith("sessionCustomChannelGroup:"):
                return api_name
    return None


def fetch_ga_period(
    session: AuthorizedSession,
    property_id: str,
    start: dt.date,
    end: dt.date,
    source_regex: str,
    channel_group_name: str,
    channel_group_api_name: Optional[str],
) -> Dict[str, Dict[str, float]]:
    metrics = [
        "sessions",
        "totalUsers",
        "engagedSessions",
        "engagementRate",
        "averageSessionDuration",
        "screenPageViews",
    ]

    total_payload = _build_report_payload(start, end, metrics)
    total_raw = _parse_single_totals(ga_post(session, property_id, total_payload), metrics)

    direct_filter = _string_filter("sessionDefaultChannelGroup", "Direct", "EXACT")
    direct_payload = _build_report_payload(start, end, metrics, dimension_filter=direct_filter)
    direct_raw = _parse_single_totals(ga_post(session, property_id, direct_payload), metrics)

    if channel_group_api_name:
        ai_filter = _string_filter(channel_group_api_name, channel_group_name, "EXACT")
        method = "channel_group"
    else:
        ai_filter = _string_filter("sessionSource", source_regex, "FULL_REGEXP")
        method = "source_regex"

    ai_payload = _build_report_payload(start, end, metrics, dimension_filter=ai_filter)
    ai_raw = _parse_single_totals(ga_post(session, property_id, ai_payload), metrics)

    organic_filter = _starts_with("sessionDefaultChannelGroup", "Organic ")
    organic_payload = _build_report_payload(start, end, metrics, dimension_filter=organic_filter)
    organic_raw = _parse_single_totals(ga_post(session, property_id, organic_payload), metrics)

    return {
        "method": method,
        "total_raw": total_raw,
        "direct_raw": direct_raw,
        "ai_raw": ai_raw,
        "organic_raw": organic_raw,
    }


def fetch_ai_source_breakdown(
    session: AuthorizedSession,
    property_id: str,
    start: dt.date,
    end: dt.date,
    source_regex: str,
) -> Dict[str, Dict[str, float]]:
    metrics = ["sessions"]
    payload = _build_report_payload(
        start,
        end,
        metrics,
        dimensions=["sessionSource"],
        dimension_filter=_string_filter("sessionSource", source_regex, "FULL_REGEXP"),
    )
    response = ga_post(session, property_id, payload)
    rows = response.get("rows", [])
    breakdown: Dict[str, Dict[str, float]] = {}
    for row in rows:
        dim_vals = row.get("dimensionValues", [])
        if not dim_vals:
            continue
        source = dim_vals[0].get("value")
        if not source:
            continue
        metrics_parsed = _parse_metrics_row(row, metrics)
        breakdown[source] = {
            "sessions": metrics_parsed.get("sessions", 0.0),
        }
    return breakdown


def fetch_ga_daily_sessions(
    session: AuthorizedSession,
    property_id: str,
    start: dt.date,
    end: dt.date,
    source_regex: str,
    channel_group_name: str,
    channel_group_api_name: Optional[str],
) -> Dict[str, Dict[str, Dict[str, float]]]:
    metrics = ["sessions"]
    dimensions = ["date"]

    total_payload = _build_report_payload(
        start,
        end,
        metrics,
        dimensions=dimensions,
        keep_empty_rows=True,
        order_by_dimension="date",
    )
    total_raw = _parse_daily(ga_post(session, property_id, total_payload), metrics)

    direct_filter = _string_filter("sessionDefaultChannelGroup", "Direct", "EXACT")
    direct_payload = _build_report_payload(
        start,
        end,
        metrics,
        dimensions=dimensions,
        dimension_filter=direct_filter,
        keep_empty_rows=True,
        order_by_dimension="date",
    )
    direct_raw = _parse_daily(ga_post(session, property_id, direct_payload), metrics)

    if channel_group_api_name:
        ai_filter = _string_filter(channel_group_api_name, channel_group_name, "EXACT")
        method = "channel_group"
    else:
        ai_filter = _string_filter("sessionSource", source_regex, "FULL_REGEXP")
        method = "source_regex"

    ai_payload = _build_report_payload(
        start,
        end,
        metrics,
        dimensions=dimensions,
        dimension_filter=ai_filter,
        keep_empty_rows=True,
        order_by_dimension="date",
    )
    ai_raw = _parse_daily(ga_post(session, property_id, ai_payload), metrics)

    organic_filter = _starts_with("sessionDefaultChannelGroup", "Organic ")
    organic_payload = _build_report_payload(
        start,
        end,
        metrics,
        dimensions=dimensions,
        dimension_filter=organic_filter,
        keep_empty_rows=True,
        order_by_dimension="date",
    )
    organic_raw = _parse_daily(ga_post(session, property_id, organic_payload), metrics)

    return {
        "method": method,
        "total": total_raw,
        "direct": direct_raw,
        "ai": ai_raw,
        "organic": organic_raw,
    }


def build_analytics_period(
    name: str,
    start: dt.date,
    end: dt.date,
    session: AuthorizedSession,
    options: GaOptions,
    channel_group_api_name: Optional[str],
) -> Dict[str, Any]:
    period_raw = fetch_ga_period(
        session,
        options.property_id,
        start,
        end,
        options.source_regex,
        options.channel_group_name,
        channel_group_api_name,
    )
    totals = allocate_direct_share(
        period_raw["total_raw"],
        period_raw["ai_raw"],
        period_raw["direct_raw"],
        options.direct_share,
    )
    direct_total = _normalize_metrics(period_raw["direct_raw"])
    direct_total_duration = direct_total["avg_session_duration"] * direct_total["sessions"]
    direct_allocated = {
        "sessions": direct_total["sessions"] * options.direct_share,
        "users": direct_total["users"] * options.direct_share,
        "engaged_sessions": direct_total["engaged_sessions"] * options.direct_share,
        "views": direct_total["views"] * options.direct_share,
        "total_duration": direct_total_duration * options.direct_share,
    }
    direct_allocated = _finalize(direct_allocated)
    organic = _normalize_metrics(period_raw["organic_raw"])
    organic_total_duration = organic["avg_session_duration"] * organic["sessions"]
    organic_totals = _finalize({**organic, "total_duration": organic_total_duration})
    ai_breakdown = fetch_ai_source_breakdown(
        session,
        options.property_id,
        start,
        end,
        options.source_regex,
    )

    daily_raw = fetch_ga_daily_sessions(
        session,
        options.property_id,
        start,
        end,
        options.source_regex,
        options.channel_group_name,
        channel_group_api_name,
    )

    trend = []
    dates = sorted(
        set(daily_raw["total"].keys())
        | set(daily_raw["ai"].keys())
        | set(daily_raw["direct"].keys())
        | set(daily_raw["organic"].keys())
    )
    for date in dates:
        total_sessions = daily_raw["total"].get(date, {}).get("sessions", 0.0)
        ai_sessions = daily_raw["ai"].get(date, {}).get("sessions", 0.0)
        direct_sessions = daily_raw["direct"].get(date, {}).get("sessions", 0.0)
        organic_sessions = daily_raw["organic"].get(date, {}).get("sessions", 0.0)
        ai_sessions_adj = ai_sessions + direct_sessions * options.direct_share
        trend.append(
            {
                "date": date,
                "ai_sessions": ai_sessions_adj,
                "organic_sessions": organic_sessions,
                "total_sessions": total_sessions,
            }
        )

    total_sessions = totals["total"]["sessions"]
    ai_share = totals["ai"]["sessions"] / total_sessions if total_sessions > 0 else 0.0

    return {
        "name": name,
        "start": start,
        "end": end,
        "totals": totals,
        "organic_totals": organic_totals,
        "ai_breakdown": ai_breakdown,
        "direct_allocated": direct_allocated,
        "trend": trend,
        "ai_share": ai_share,
        "method": period_raw["method"],
        "direct_share": options.direct_share,
    }


def build_analytics(
    creds: Credentials,
    options: GaOptions,
    last_date: dt.date,
    periods: List[int],
    max_workers: int,
) -> Dict[str, Any]:
    session = AuthorizedSession(creds)
    print("Fetching GA4 analytics metadata...")
    channel_group_api_name = resolve_channel_group_api_name(
        session,
        options.property_id,
        options.channel_group_name,
        options.channel_group_id,
    )

    def make_period_task(days: int):
        def _task():
            end = last_date
            start = end - dt.timedelta(days=days - 1)
            name = f"Last {days} days"
            print(f"Fetching GA4 data: {name}")
            period_session = AuthorizedSession(creds)
            return build_analytics_period(
                name,
                start,
                end,
                period_session,
                options,
                channel_group_api_name,
            )
        return _task

    max_workers = max(1, int(max_workers))
    analytics_periods = []
    tasks = {days: make_period_task(days) for days in periods}
    from concurrent.futures import ThreadPoolExecutor, as_completed
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_map = {executor.submit(task): days for days, task in tasks.items()}
        for future in as_completed(future_map):
            analytics_periods.append(future.result())

    analytics_periods.sort(key=lambda p: periods.index(int(p["name"].split()[1])))

    analytics_by_name = {p["name"]: p for p in analytics_periods}
    for days in periods:
        name = f"Last {days} days"
        prev_end = last_date - dt.timedelta(days=days)
        prev_start = prev_end - dt.timedelta(days=days - 1)
        prev_period = build_analytics_period(
            name,
            prev_start,
            prev_end,
            AuthorizedSession(creds),
            options,
            channel_group_api_name,
        )
        if name in analytics_by_name:
            analytics_by_name[name]["pop_totals"] = prev_period["totals"]
            analytics_by_name[name]["pop_organic_totals"] = prev_period["organic_totals"]

    for days in periods:
        name = f"Last {days} days"
        yoy_end = last_date - dt.timedelta(days=365)
        yoy_start = yoy_end - dt.timedelta(days=days - 1)
        yoy_period = build_analytics_period(
            name,
            yoy_start,
            yoy_end,
            AuthorizedSession(creds),
            options,
            channel_group_api_name,
        )
        if name in analytics_by_name:
            analytics_by_name[name]["yoy_totals"] = yoy_period["totals"]
            analytics_by_name[name]["yoy_organic_totals"] = yoy_period["organic_totals"]

    for period in analytics_periods:
        period.setdefault("pop_totals", period["totals"])
        period.setdefault("pop_organic_totals", period["organic_totals"])
        period.setdefault("yoy_totals", period["totals"])
        period.setdefault("yoy_organic_totals", period["organic_totals"])

    method = "channel_group" if channel_group_api_name else "source_regex"
    return {
        "periods": analytics_periods,
        "direct_share": options.direct_share,
        "source_regex": options.source_regex,
        "channel_group_name": options.channel_group_name,
        "method": method,
    }
