#=============================================================
###  CREATED AT     : 24 FEBRUARI 2026                         
###  UPDATED AT     : 24 FEBRUARI 2026                          
###  COPYRIGHT      : BRIBRAIN DATA ENGINEER TEAM           
###  DESCRIPTION    : Module untuk kumpulan metadata        
#=============================================================

import os
import re
from email.utils import parseaddr

from pytz import timezone
from datetime import datetime
import subprocess

## ===================================================================================================

def _get_env(name: str) -> str | None:
    """
    Mengambil environment variable.

    Fungsi ini digunakan sebagai helper internal untuk mengambil
    nilai environment variable.

    Args:
        name (str):
            Nama environment variable.

    Returns:
        str | None:
            Nilai dari environment variable tersebut,
            atau None jika tidak ditemukan atau kosong.
    """
    value = os.getenv(name)

    if not value:
        return None

    return value

## ===================================================================================================
  
def get_userid() -> str | None:
    """
    Mengambil Hadoop user ID dari environment variable.

    Returns:
        str | None:
            User ID Hadoop yang tersimpan pada
            environment variable `HADOOP_USER_NAME`,
            atau None jika tidak tersedia.

    Example:
        >>> get_userid()
        'bribrain_sac'
    """
    return _get_env("HADOOP_USER_NAME")

## ===================================================================================================

def get_username() -> str | None:
    """
    Menghasilkan nama lengkap berdasarkan Git author email.

    Nama akan dibentuk dari bagian local email
    (sebelum '@') dengan memisahkan kata menggunakan
    delimiter umum seperti titik (.), underscore (_),
    atau tanda minus (-).

    Returns:
        str | None:
            Nama lengkap dalam format kapital di setiap kata,
            atau None jika `GIT_AUTHOR_EMAIL` tidak tersedia
            atau tidak valid.

    Example:
        >>> get_username()
        'Andri Ariyanto'
    """
    email = _get_env("GIT_AUTHOR_EMAIL")

    if email is None:
        return None

    _, addr = parseaddr(email)

    if not addr or "@" not in addr:
        return None

    local_part = addr.split("@", 1)[0]

    # Split on common separators: . _ -
    words = re.split(r"[._-]+", local_part)

    return " ".join(word.capitalize() for word in words if word)

## ===================================================================================================

def get_email() -> str | None:
    """
    Mengambil Git author email dari environment variable.

    Returns:
        str | None:
            Email yang tersimpan pada
            environment variable `GIT_AUTHOR_EMAIL`,
            atau None jika tidak tersedia.

    Example:
        >>> get_email()
        'user@example.com'
    """
    return _get_env("GIT_AUTHOR_EMAIL")

## ===================================================================================================

def get_project_url() -> str | None:
    """
    Mengambil URL project dari environment variable.

    Returns:
        str | None:
            URL project yang tersimpan pada
            environment variable `CDSW_PROJECT_URL`,
            atau None jika tidak tersedia.
    """
    return _get_env("CDSW_PROJECT_URL")

## ===================================================================================================

def get_project_id() -> str | None:
    """
    Mengambil ID project dari environment variable.

    Returns:
        str | None:
            ID project yang tersimpan pada
            environment variable `CDSW_PROJECT_ID`,
            atau None jika tidak tersedia.
    """
    return _get_env("CDSW_PROJECT_ID")

## ===================================================================================================

def get_project_num() -> str | None:
    """
    Mengambil nomor project dari environment variable.

    Returns:
        str | None:
            Nomor project yang tersimpan pada
            environment variable `CDSW_PROJECT_NUM`,
            atau None jika tidak tersedia.
    """
    return _get_env("CDSW_PROJECT_NUM")

## ===================================================================================================

def get_project_name(env: str | None = None, case: str | None = None, delimiter: str | None = "_") -> str | None:
    """
    Membentuk nama project dengan format yang dapat disesuaikan.

    Nama project diambil dari environment variable `CDSW_PROJECT`,
    kemudian dapat dimodifikasi berdasarkan environment deployment,
    format huruf, dan delimiter.

    Args:
        env (str | None, optional):
            Environment deployment.

            Pilihan:
            - None   : tanpa prefix
            - "dev"  : menambahkan prefix `dev`
            - "prod" : menambahkan prefix `prod`

            Default: None

        case (str | None, optional):
            Mengatur format huruf pada hasil akhir.

            Pilihan:
            - None   : mempertahankan format asli
            - "lower": mengubah seluruh teks menjadi huruf kecil
            - "upper": mengubah seluruh teks menjadi huruf besar

            Default: None

        delimiter (str | None, optional):
            Karakter pemisah untuk menggantikan tanda minus ("-").

            - Jika string diberikan → semua "-" diganti.
            - Jika None → delimiter asli dipertahankan.

            Default: "_"

    Returns:
        str | None:
            Nama project yang telah diformat,
            atau None jika `CDSW_PROJECT` tidak tersedia.

    Raises:
        TypeError:
            Jika tipe parameter `env`, `case`, atau `delimiter`
            tidak valid.
        ValueError:
            Jika nilai parameter `env` atau `case` tidak valid.

    Example:
        >>> get_project_name()
        'bribrain_pipeline'

        >>> get_project_name(env="dev", case="upper")
        'DEV_BRIBRAIN_PIPELINE'

        >>> get_project_name(delimiter=None)
        'dev-bribrain-pipeline'
    """
    if env is not None and not isinstance(env, str):
        raise TypeError(f"env must be str or None, got {type(env).__name__}")

    if case is not None and not isinstance(case, str):
        raise TypeError(f"case must be str or None, got {type(case).__name__}")

    if delimiter is not None and not isinstance(delimiter, str):
        raise TypeError(f"delimiter must be str or None, got {type(delimiter).__name__}")

    if env not in {"dev", "prod", None}:
        raise ValueError("env must be 'dev' or 'prod' or None")

    if case not in {None, "lower", "upper"}:
        raise ValueError("case must be None, 'lower', or 'upper'")

    # extract project name
    project = _get_env("CDSW_PROJECT")

    if project is None:
        return None

    # Change delimiter ONLY if provided
    if delimiter is not None:
        project = project.replace("-", delimiter)
        join_delimiter = delimiter
    else:
        join_delimiter = "-"  # use original delimiter

    # Add prefix BEFORE case transformation
    if env == "dev":
        project = f"dev{join_delimiter}{project}"
    elif env == "prod":
        project = f"prod{join_delimiter}{project}"

    # Apply case to entire string
    if case == "lower":
        project = project.lower()
    elif case == "upper":
        project = project.upper()

    return project

## ===================================================================================================

def get_proxy() -> dict | None:
    """
    Mengambil konfigurasi proxy HTTP dan HTTPS
    dari environment variable.

    Returns:
        dict | None:
            Dictionary berisi konfigurasi proxy yang tersedia,
            atau None jika tidak ada proxy yang diset.

    Example:
        >>> get_proxy()
        {
            "http": "http://proxy.company.com:8080",
            "https": "https://proxy.company.com:8080"
        }
    """
    proxies = {}

    http_proxy = _get_env("http_proxy")
    https_proxy = _get_env("https_proxy")

    if http_proxy:
        proxies["http"] = http_proxy

    if https_proxy:
        proxies["https"] = https_proxy

    return proxies if proxies else None

## ===================================================================================================

def get_schema_space(schema: str) -> dict | None:
    """
    Mengambil informasi penggunaan ruang HDFS dari suatu schema.

    Args:
        schema (str): nama schema di Hive/HDFS

    Returns:
        dict | None: dictionary berisi informasi space quota,
            atau None jika terjadi error atau data tidak ditemukan.

            Keys:
            - source (str): nama schema
            - space_quota (float): total kuota dalam bytes
            - remaining_space_quota (float): sisa kuota dalam bytes
            - remaining_space_percentage (float): persentase sisa kuota
            - dttm_ingest (str): waktu pengambilan data
    """

    current_time = datetime.now(timezone("Asia/Jakarta")).strftime("%Y-%m-%d %H:%M:%S")

    if schema.startswith("dev_") or schema == "dgv":
        hdfs_path = f"/dev/{schema}.db"
    else:
        hdfs_path = f"/user/hive/warehouse/{schema}.db"

    # Construct command: hdfs dfs -count -q /path/to/db
    cmd = f"hdfs dfs -count -q {hdfs_path}"

    # Execute command
    result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    # Parse Output
    # Format: QUOTA REM_QUOTA SPACE_QUOTA REM_SPACE_QUOTA DIR_COUNT FILE_COUNT CONTENT_SIZE PATH
    if result.returncode != 0 or not result.stdout.strip():
        return None

    parts = result.stdout.strip().split()

    # Extract Raw Values
    space_quota = float(parts[2])
    rem_space_quota = float(parts[3])
    full_path = parts[7]

    # Calculate Percentage
    # Avoid division by zero if quota is missing/infinite (set to 100% free if no quota)
    if space_quota > 0:
        rem_pct = (rem_space_quota / space_quota) * 100
    else:
        rem_pct = 100.0

    # Clean Source Name (e.g., 'sharingvision.db' -> 'sharingvision')
    source_name = full_path.split('/')[-1].replace('.db', '')

    return {
        'schema': source_name,
        'space_quota': space_quota,
        'remaining_space_quota': rem_space_quota,
        'remaining_space_percentage': rem_pct,
        'position_date': current_time
    }

## ===================================================================================================

def get_table_count_files(spark, schema: str, table: str) -> int | None:
    """
    Mengambil jumlah file dari sebuah table di Hive menggunakan Hadoop FileSystem API.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        int | None: jumlah file dari table, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        content_summary = fs.getContentSummary(Path(hdfs_table_path))
        return content_summary.getFileCount()

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_table_average_files(spark, schema: str, table: str) -> float | None:
    """
    Mengambil rata-rata jumlah file per partisi dari sebuah table di Hive
    menggunakan Hadoop FileSystem API.

    Mendukung nested partition (misal: year/month/day) dengan melakukan
    rekursi hingga ke leaf partition (partisi terdalam yang berisi file).

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        float | None: rata-rata jumlah file per partisi,
            atau None jika terjadi error atau tidak ada partisi
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        # Recursively find leaf partitions and count files
        def count_leaf_files(path):
            """Rekursi ke leaf partition, return list of file counts."""
            status_array = fs.listStatus(Path(path))
            subdirs = [s for s in status_array if s.isDirectory()]
            files = [s for s in status_array if not s.isDirectory()]

            if not subdirs:
                # Leaf partition: return file count
                return [len(files)]

            # Has subdirectories: recurse deeper
            counts = []
            for d in subdirs:
                counts.extend(count_leaf_files(d.getPath().toString()))
            return counts

        file_counts = count_leaf_files(hdfs_table_path)

        if not file_counts:
            return None

        return sum(file_counts) / len(file_counts)

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_partition_count_files(spark, schema: str, table: str, partition) -> int | None:
    """
    Mengambil jumlah file dari sebuah partisi tertentu di Hive
    menggunakan Hadoop FileSystem API.

    Mendukung nested partition (misal: year/month/day) dengan melakukan
    rekursi hingga ke leaf partition.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table
        partition (str | dict): partisi yang ingin dihitung jumlah filenya
            - str  : path partisi (contoh: "ds=202508")
            - dict : dictionary partisi dari get_list_partition
                     (contoh: {"ds": "202508"})

    Returns:
        int | None: jumlah file dari partisi, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        # Build partition path
        if isinstance(partition, dict):
            partition_parts = "/".join(
                f"{k}={v}" for k, v in partition.items()
                if k != "__formatted_partition"
            )
        elif isinstance(partition, str):
            partition_parts = partition
        else:
            raise TypeError(
                f"partition must be str or dict, got {type(partition).__name__}"
            )

        hdfs_partition_path = f"{hdfs_table_path}/{partition_parts}"

        content_summary = fs.getContentSummary(Path(hdfs_partition_path))
        return content_summary.getFileCount()

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_table_size(spark, schema: str, table: str) -> int | None:
    """
    Mengambil ukuran file dari sebuah table di Hive menggunakan Hadoop FileSystem API.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        int | None: ukuran file dari table, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        content_summary = fs.getContentSummary(Path(hdfs_table_path))
        return content_summary.getLength()

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_table_average_size(spark, schema: str, table: str) -> float | None:
    """
    Mengambil rata-rata ukuran per partisi dari sebuah table di Hive
    menggunakan Hadoop FileSystem API.

    Mendukung nested partition (misal: year/month/day) dengan melakukan
    rekursi hingga ke leaf partition (partisi terdalam yang berisi file).

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        float | None: rata-rata ukuran (bytes) per partisi,
            atau None jika terjadi error atau tidak ada partisi
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        # Recursively find leaf partitions and get size
        def get_leaf_sizes(path):
            """Rekursi ke leaf partition, return list of sizes (bytes)."""
            status_array = fs.listStatus(Path(path))
            subdirs = [s for s in status_array if s.isDirectory()]

            if not subdirs:
                # Leaf partition: get total size
                content_summary = fs.getContentSummary(Path(path))
                return [content_summary.getLength()]

            # Has subdirectories: recurse deeper
            sizes = []
            for d in subdirs:
                sizes.extend(get_leaf_sizes(d.getPath().toString()))
            return sizes

        partition_sizes = get_leaf_sizes(hdfs_table_path)

        if not partition_sizes:
            return None

        return sum(partition_sizes) / len(partition_sizes)

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_partition_size(spark, schema: str, table: str, partition) -> int | None:
    """
    Mengambil ukuran file dari sebuah partisi tertentu di Hive
    menggunakan Hadoop FileSystem API.

    Mendukung nested partition (misal: year/month/day) dengan melakukan
    rekursi hingga ke leaf partition.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table
        partition (str | dict): partisi yang ingin dihitung jumlah filenya
            - str  : path partisi (contoh: "ds=202508")
            - dict : dictionary partisi dari get_list_partition
                     (contoh: {"ds": "202508"})

    Returns:
        int | None: ukuran file dari partisi, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        # Build partition path
        if isinstance(partition, dict):
            partition_parts = "/".join(
                f"{k}={v}" for k, v in partition.items()
                if k != "__formatted_partition"
            )
        elif isinstance(partition, str):
            partition_parts = partition
        else:
            raise TypeError(
                f"partition must be str or dict, got {type(partition).__name__}"
            )

        hdfs_partition_path = f"{hdfs_table_path}/{partition_parts}"

        content_summary = fs.getContentSummary(Path(hdfs_partition_path))
        return content_summary.getLength()

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_table_last_modified(spark, schema: str, table: str) -> int | None:
    """
    Mengambil waktu modifikasi terakhir dari sebuah table di Hive menggunakan Hadoop FileSystem API.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        int | None: waktu modifikasi terakhir dari table, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        file_status = fs.getFileStatus(Path(hdfs_table_path))
        m_time_ms = file_status.getModificationTime()
        return datetime.fromtimestamp(m_time_ms / 1000.0)

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

def get_table_last_access(spark, schema: str, table: str) -> int | None:
    """
    Mengambil waktu akses terakhir dari sebuah table di Hive menggunakan Hadoop FileSystem API.

    Args:
        spark: SparkSession
        schema (str): nama schema dari table di hive
        table (str): nama table

    Returns:
        int | None: waktu akses terakhir dari table, atau None jika terjadi error
    """
    try:
        Path = lambda p: spark._jvm.org.apache.hadoop.fs.Path(p)
        fs = spark._jvm.org.apache.hadoop.fs.FileSystem.get(spark._jsc.hadoopConfiguration())

        # Determine the hdfs_path
        if schema.startswith("dev_") or schema == "dgv":
            hdfs_table_path = f"/dev/{schema}.db/{table}"
        else:
            hdfs_table_path = f"/user/hive/warehouse/{schema}.db/{table}"

        file_status = fs.getFileStatus(Path(hdfs_table_path))
        a_time_ms = file_status.getAccessTime()
        return datetime.fromtimestamp(a_time_ms / 1000.0)

    except Exception as e:
        print(f"Error: {e}")
        return None

## ===================================================================================================

