from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Iterable, Mapping, Sequence

DEFAULT_VERSION = "2.2"

_VERSION_ALIASES: dict[str, str] = {
    "2.1": "2.1.1",
    "2.1.1": "2.1.1",
    "2.2": "2.2",
    "2.2.1": "2.2",
    "2.3": "2.3",
}

SUPPORTED_MODULES = {"locations", "tariffs", "tokens", "sessions", "cdrs"}


@dataclass(frozen=True)
class ValidationIssue:
    row: int
    level: str  # "error" | "warning"
    code: str
    field: str | None = None
    detail: str | None = None
    version: str | None = None


_BASE_SCHEMAS: dict[str, Mapping[str, Any]] = {
    "locations": {
        "type": "object",
        "required": ["id", "evses", "last_updated"],
        "properties": {
            "id": {"type": "string"},
            "country_code": {"type": "string"},
            "party_id": {"type": "string"},
            "name": {"type": "string"},
            "address": {"type": "string"},
            "city": {"type": "string"},
            "postal_code": {"type": "string"},
            "last_updated": {"type": "string", "format": "date-time"},
            "evses": {
                "type": "array",
                "items": {
                    "type": "object",
                    "required": ["uid", "status"],
                    "properties": {
                        "uid": {"type": "string"},
                        "evse_id": {"type": "string"},
                        "status": {"type": "string"},
                        "connectors": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "required": ["id", "standard", "format"],
                                "properties": {
                                    "id": {"type": "string"},
                                    "standard": {"type": "string"},
                                    "format": {"type": "string"},
                                    "power_type": {"type": "string"},
                                    "max_amperage": {"type": "number"},
                                    "max_voltage": {"type": "number"},
                                },
                            },
                        },
                    },
                },
            },
        },
    },
    "tariffs": {
        "type": "object",
        "required": ["id", "currency", "elements", "last_updated"],
        "properties": {
            "id": {"type": "string"},
            "currency": {"type": "string"},
            "elements": {
                "type": "array",
            },
            "price_components": {"type": "array"},
            "last_updated": {"type": "string", "format": "date-time"},
        },
    },
    "tokens": {
        "type": "object",
        "required": ["uid", "type", "auth_id", "issuer", "last_updated"],
        "properties": {
            "uid": {"type": "string"},
            "type": {
                "type": "string",
                "enum": ["AD_HOC_USER", "APP_USER", "RFID", "REMOTE", "OTHER"],
            },
            "whitelist": {
                "type": "string",
                "enum": [
                    "ALLOWED",
                    "ALLOWED_LOCAL",
                    "ALLOWED_OFFLINE",
                    "ALWAYS",
                    "BLOCKED",
                    "EXPIRED",
                    "NEVER",
                    "NO_LONGER_VALID",
                    "WHITELISTED",
                ],
            },
            "issuer": {"type": "string"},
            "contract_id": {"type": "string"},
            "visual_number": {"type": "string"},
            "status": {
                "type": "string",
                "enum": ["ACTIVE", "BLOCKED", "EXPIRED", "INACTIVE", "VALID"],
            },
            "auth_id": {"type": "string"},
            "country_code": {"type": "string"},
            "party_id": {"type": "string"},
            "valid": {"type": "boolean"},
            "valid_until": {"type": "string", "format": "date-time"},
            "last_updated": {"type": "string", "format": "date-time"},
        },
    },
    "sessions": {
        "type": "object",
        "required": ["id", "start_date_time", "last_updated", "status"],
        "properties": {
            "id": {"type": "string"},
            "country_code": {"type": "string"},
            "party_id": {"type": "string"},
            "start_date_time": {"type": "string", "format": "date-time"},
            "end_date_time": {"type": "string", "format": "date-time"},
            "last_updated": {"type": "string", "format": "date-time"},
            "kwh": {"type": "number"},
            "location_id": {"type": "string"},
            "evse_uid": {"type": "string"},
            "connector_id": {"type": "string"},
            "status": {"type": "string"},
        },
    },
    "cdrs": {
        "type": "object",
        "required": ["id", "start_date_time", "total_energy"],
        "properties": {
            "id": {"type": "string"},
            "cdr_id": {"type": "string"},
            "transaction_id": {"type": "string"},
            "start_date_time": {"type": "string", "format": "date-time"},
            "stop_date_time": {"type": "string", "format": "date-time"},
            "total_cost": {"type": "object"},
            "total_energy": {"type": "number"},
            "total_time": {"type": "number"},
            "last_updated": {"type": "string", "format": "date-time"},
            "charging_periods": {"type": "array"},
            "cdr_token": {
                "type": "object",
                "required": ["uid", "type"],
                "properties": {
                    "uid": {"type": "string"},
                    "type": {"type": "string"},
                },
            },
        },
    },
}

_VERSION_SCHEMAS: dict[str, Mapping[str, Mapping[str, Any]]] = {
    "2.1.1": _BASE_SCHEMAS,
    "2.2": _BASE_SCHEMAS,
    "2.3": _BASE_SCHEMAS,
}


def _schema_for(module: str, version: str) -> Mapping[str, Any]:
    version_map = _VERSION_SCHEMAS.get(version, _VERSION_SCHEMAS.get(DEFAULT_VERSION, {}))
    return version_map.get(module, {})


def _path_join(base: str, segment: str) -> str:
    if not base:
        return segment
    if segment.startswith("["):
        return f"{base}{segment}"
    return f"{base}.{segment}"


def _validate_schema(
    entry: Any,
    schema: Mapping[str, Any],
    row_number: int,
    version: str,
    path: str = "",
) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    if not schema:
        return issues

    expected_type = schema.get("type")
    type_map: dict[str, Any] = {
        "object": Mapping,
        "array": list,
        "string": str,
        "number": (int, float),
        "boolean": bool,
    }
    if expected_type:
        expected = type_map.get(expected_type)
        if expected and not isinstance(entry, expected):
            issues.append(
                ValidationIssue(
                    row=row_number,
                    level="error",
                    code="invalid_type",
                    field=path or "(root)",
                    detail=f"expected {expected_type}",
                    version=version,
                )
            )
            return issues

    if schema.get("enum") and entry not in schema["enum"]:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="invalid_choice",
                field=path or "(root)",
                detail=str(entry),
                version=version,
            )
        )

    if schema.get("format") == "date-time":
        if entry and not _looks_like_timestamp(entry):
            issues.append(
                ValidationIssue(
                    row=row_number,
                    level="error",
                    code="invalid_timestamp",
                    field=path or "(root)",
                    detail=str(entry),
                    version=version,
                )
            )
        return issues

    if isinstance(entry, Mapping):
        required_fields = schema.get("required", [])
        for field in required_fields:
            if entry.get(field) in (None, ""):
                issues.append(
                    ValidationIssue(
                        row=row_number,
                        level="error",
                        code="missing_field",
                        field=_path_join(path, field),
                        version=version,
                    )
                )
        properties = schema.get("properties", {})
        for key, subschema in properties.items():
            if key not in entry or entry.get(key) in (None, ""):
                continue
            issues.extend(
                _validate_schema(
                    entry.get(key),
                    subschema,
                    row_number,
                    version,
                    _path_join(path, key),
                )
            )
    elif isinstance(entry, list):
        item_schema = schema.get("items")
        if item_schema:
            for idx, value in enumerate(entry):
                issues.extend(
                    _validate_schema(
                        value,
                        item_schema,
                        row_number,
                        version,
                        _path_join(path, f"[{idx}]"),
                    )
                )
    return issues


def normalize_version(raw_version: str | None, *, default: str = DEFAULT_VERSION) -> str:
    if not raw_version:
        return default
    normalized = str(raw_version).strip().lower()
    if normalized in _VERSION_ALIASES:
        return _VERSION_ALIASES[normalized]
    raise ValueError(f"Unsupported OCPI version: {raw_version}")


def validate_payloads(
    module: str,
    version: str,
    entries: Sequence[Any],
) -> list[ValidationIssue]:
    normalized_module = (module or "").lower()
    if normalized_module not in SUPPORTED_MODULES:
        raise ValueError(f"Unsupported OCPI module: {module}")

    normalized_version = normalize_version(version)
    issues: list[ValidationIssue] = []
    schema = _schema_for(normalized_module, normalized_version)
    for idx, entry in enumerate(entries):
        row_number = idx + 1
        issues.extend(_validate_schema(entry, schema, row_number, normalized_version))
        if not isinstance(entry, Mapping):
            continue
        if normalized_module == "locations":
            issues.extend(_validate_location(entry, row_number, normalized_version))
        elif normalized_module == "tariffs":
            issues.extend(_validate_tariff(entry, row_number, normalized_version))
        elif normalized_module == "tokens":
            issues.extend(_validate_token(entry, row_number, normalized_version))
        elif normalized_module == "sessions":
            issues.extend(_validate_session(entry, row_number, normalized_version))
        elif normalized_module == "cdrs":
            issues.extend(_validate_cdr(entry, row_number, normalized_version))

    # Attach a summary warning to indicate which version was used
    issues.append(
        ValidationIssue(
            row=0,
            level="warning",
            code="validated_version",
            detail=normalized_version,
            version=normalized_version,
        )
    )
    return issues


def _validate_location(entry: Mapping[str, Any], row_number: int, version: str) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    if not entry.get("id"):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="missing_field",
                field="id",
                version=version,
            )
        )
    evses = entry.get("evses")
    if evses is None or (isinstance(evses, list) and len(evses) == 0):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="empty_list",
                field="evses",
                version=version,
            )
        )
    elif not isinstance(evses, list):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="invalid_array",
                field="evses",
                version=version,
            )
        )
    else:
        for idx, evse in enumerate(evses):
            if not isinstance(evse, Mapping):
                issues.append(
                    ValidationIssue(
                        row=row_number,
                        level="warning",
                        code="invalid_type",
                        field=f"evses[{idx}]",
                        detail="EVSE entry",
                        version=version,
                    )
                )
                continue
            if not evse.get("uid"):
                issues.append(
                    ValidationIssue(
                        row=row_number,
                        level="warning",
                        code="missing_field",
                        field=f"evses[{idx}].uid",
                        version=version,
                    )
                )
            connectors = evse.get("connectors")
            if connectors is not None and not isinstance(connectors, list):
                issues.append(
                    ValidationIssue(
                        row=row_number,
                        level="warning",
                        code="invalid_array",
                        field=f"evses[{idx}].connectors",
                        version=version,
                    )
                )
    return issues


def _validate_tariff(entry: Mapping[str, Any], row_number: int, version: str) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    for field in ("id", "currency"):
        if not entry.get(field):
            issues.append(
                ValidationIssue(
                    row=row_number,
                    level="error",
                    code="missing_field",
                    field=field,
                    version=version,
                )
            )
    elements = entry.get("elements") or entry.get("price_components")
    if elements is None or elements == "":
        issues.append(
            ValidationIssue(
                row=row_number,
                level="warning",
                code="empty_list",
                field="elements",
                version=version,
            )
        )
    if elements and not isinstance(elements, (list, str)):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="invalid_array",
                field="elements",
                version=version,
            )
        )
    return issues


def _validate_token(entry: Mapping[str, Any], row_number: int, version: str) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    if not entry.get("uid"):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="missing_field",
                field="uid",
                version=version,
            )
        )
    token_type = entry.get("type")
    if not token_type:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="warning",
                code="missing_token_type",
                field="type",
                version=version,
            )
        )
    status = entry.get("status")
    if status and str(status).lower() not in {"valid", "expired", "blocked", "inactive"}:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="warning",
                code="unknown_status",
                field="status",
                detail=str(status),
                version=version,
            )
        )
    return issues


def _validate_session(entry: Mapping[str, Any], row_number: int, version: str) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    session_id = entry.get("id") or entry.get("session_id") or entry.get("uid")
    if not session_id:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="missing_field",
                field="id",
                version=version,
            )
        )
    status = entry.get("status")
    if status and str(status).upper() not in {"ACTIVE", "COMPLETED", "INVALID", "PENDING"}:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="warning",
                code="unknown_status",
                field="status",
                detail=str(status),
                version=version,
            )
        )
    return issues


def _validate_cdr(entry: Mapping[str, Any], row_number: int, version: str) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    cdr_id = entry.get("id") or entry.get("cdr_id") or entry.get("transaction_id")
    if not cdr_id:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="missing_field",
                field="id",
                version=version,
            )
        )

    start_ts, start_key = _first_present(entry, ("start_date_time", "start_datetime", "startDateTime"))
    stop_ts, stop_key = _first_present(entry, ("stop_date_time", "stop_datetime", "stopDateTime"))
    if not start_ts:
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="missing_field",
                field="start_date_time",
                version=version,
            )
        )
    elif not entry.get("start_date_time") and not _looks_like_timestamp(start_ts):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="invalid_timestamp",
                field=start_key or "start_date_time",
                version=version,
            )
        )
    if stop_ts and not (entry.get("stop_date_time") and stop_key == "stop_date_time") and not _looks_like_timestamp(stop_ts):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="error",
                code="invalid_timestamp",
                field=stop_key or "stop_date_time",
                version=version,
            )
        )

    charging_periods = entry.get("charging_periods")
    if charging_periods is not None and not isinstance(charging_periods, list):
        issues.append(
            ValidationIssue(
                row=row_number,
                level="warning",
                code="invalid_array",
                field="charging_periods",
                version=version,
            )
        )
    return issues


def _looks_like_timestamp(value: Any) -> bool:
    try:
        if isinstance(value, datetime):
            return True
        text = str(value)
        if not re.match(r"^\d{4}-\d{2}-\d{2}T", text):
            return False
        datetime.fromisoformat(text.replace("Z", "+00:00"))
        return True
    except Exception:
        return False


def _first_present(entry: Mapping[str, Any], keys: Iterable[str]) -> tuple[Any, str | None]:
    for key in keys:
        if entry.get(key) not in (None, ""):
            return entry.get(key), key
    return None, None
