'''
    PM4Py – A Process Mining Library for Python
Copyright (C) 2024 Process Intelligence Solutions UG (haftungsbeschränkt)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see this software project's root or
visit <https://www.gnu.org/licenses/>.

Website: https://processintelligence.solutions
Contact: info@processintelligence.solutions
'''

import os
import sqlite3
from datetime import datetime, timedelta
import pandas as pd
from enum import Enum
from typing import Optional, Dict, Any

from pm4py.util import exec_utils, pandas_utils


class Parameters(Enum):
    HISTORY_DB_PATH = "history_db_path"


# ---------------------------------------------------------------------------
# Helper: decode the transition core type stored in Chrome history
# ---------------------------------------------------------------------------
_transition_core_map = {
    0: "LINK",  # user clicked a link
    1: "TYPED",  # user typed the URL in the omnibox
    2: "AUTO_BOOKMARK",  # URL was bookmarked and launched
    3: "AUTO_SUBFRAME",  # sub‑frame navigation (auto)
    4: "MANUAL_SUBFRAME",  # sub‑frame navigation (manual)
    5: "GENERATED",  # generated by the browser (e.g. search)
    6: "START_PAGE",  # start‑up or home page
    7: "FORM_SUBMIT",  # form submission
    8: "RELOAD",  # explicit reload (F5, ⟳)
    9: "KEYWORD",  # keyword search in omnibox
    10: "KEYWORD_GENERATED",  # keyword search generated URL
}


def _decode_transition(value: int) -> str:
    """Return the human‑readable core transition type.

    Chrome encodes qualifiers in the upper bits; the lower 8 bits (0xFF) store
    the *core* transition type (enum content/public/common/page_transition_types.h).
    """
    core = value & 0xFF
    return _transition_core_map.get(core, f"UNKNOWN_{core}")


# ---------------------------------------------------------------------------
# Main extractor
# ---------------------------------------------------------------------------

def apply(parameters: Optional[Dict[Any, str]] = None) -> pd.DataFrame:
    """Extract a dataframe containing the navigation history of Google Chrome.

    **Event log semantics**
    -----------------------
    * **CASE ID** (``case:concept:name``) browser *profile* name (e.g. "Profile 1")
    * **ACTIVITY** (``concept:name``) full path of the visited page ("example.com/path"), *without* query parameters
    * **TIMESTAMP** (``time:timestamp``) local timestamp of the visit (start of the page view)

    **Additional attributes**
    --------------------------------
    * ``title`` page title as recorded by Chrome (empty if unavailable)
    * ``visit_duration`` seconds spent on the page (0 if unknown / Chrome <93)
    * ``transition`` human‑readable page transition (LINK, TYPED, …)
    * ``typed_count`` how many times the user manually typed this URL
    * ``visit_count`` how many *total* visits Chrome knows for this URL
    * ``referrer`` URL of the *from_visit* (previous page) if available

    Parameters
    ----------
    Parameters.HISTORY_DB_PATH : str, optional
        Path to the **folder** that contains the Chrome profile directories or to
        a single *History* SQLite file. Defaults to the standard Windows path of
        the current user (``%LOCALAPPDATA%\\Google\\Chrome\\User Data``).

    Returns
    -------
    pd.DataFrame
        PM4Py‑compatible event log where each row is a page visit.
    """

    if parameters is None:
        parameters = {}

    history_db_path = exec_utils.get_param_value(
        Parameters.HISTORY_DB_PATH,
        parameters,
        os.path.join(
            os.getenv("LOCALAPPDATA", ""), "Google", "Chrome", "User Data"
        ),
    )

    # ---------------------------------------------------------------------
    # Locate profile history files
    # ---------------------------------------------------------------------
    if os.path.isdir(history_db_path):
        profiles = [
            (os.path.join(history_db_path, prof, "History"), prof)
            for prof in os.listdir(history_db_path)
            if prof.startswith("Profile ") or prof == "Default"
        ]
    else:
        # user passed a single file instead of a directory
        profiles = [(history_db_path, "DEFAULT")]

    profiles = [p for p in profiles if os.path.exists(p[0])]

    events = []
    for history_file, profile_name in profiles:
        # open read‑only – Chrome keeps a *Write‑Ahead Log* that blocks writing
        conn = sqlite3.connect(f"file:{history_file}?mode=ro", uri=True)
        cur = conn.cursor()

        # Check if visit_duration column exists (Chrome 93+)
        cur.execute("PRAGMA table_info(visits)")
        visit_cols = [row[1] for row in cur.fetchall()]
        has_duration = "visit_duration" in visit_cols

        # Build query dynamically based on column availability
        query = f"""
            SELECT
                urls.url AS full_url,
                urls.title AS title,
                urls.typed_count AS typed_cnt,
                urls.visit_count AS visit_cnt,
                visits.visit_time AS visit_time,
                { 'visits.visit_duration' if has_duration else '0' } AS duration,
                visits.transition AS transition,
                visits.from_visit AS from_visit_id
            FROM visits
            JOIN urls ON visits.url = urls.id
        """
        cur.execute(query)
        rows = cur.fetchall()

        # Build *id → url* map so we can resolve from_visit later without a JOIN
        cur.execute("SELECT id, url FROM urls")
        id2url = dict(cur.fetchall())

        for (
            full_url,
            title,
            typed_cnt,
            visit_cnt,
            visit_time,
            duration,
            transition_val,
            from_visit_id,
        ) in rows:
            # ----------------------------------------------------------------
            # Convert Chromium timestamps (µs since 1601‑01‑01) → datetime
            # ----------------------------------------------------------------
            # Re‑implement the SQL the original script used so that we avoid
            # timezone surprises and keep everything in Python for clarity.
            visit_start = datetime(1601, 1, 1) + timedelta(microseconds=visit_time)
            visit_start = visit_start.astimezone()  # convert to local tz

            # Duration is stored in *microseconds* (uint32) – Chrome <93 omitted it
            if has_duration and duration:
                visit_duration = duration / 1_000_000.0
            else:
                visit_duration = 0.0

            referrer_url = id2url.get(from_visit_id, "")

            ev = {
                "case:concept:name": profile_name,
                "concept:name": full_url.split("//")[-1].split("?")[0].replace(",", ""),
                "complete_url": full_url,
                "domain": full_url.split("//")[-1].split("/")[0],
                "url_wo_parameters": full_url.split("//")[-1].split("?")[0],
                "time:timestamp": visit_start,
                # New attributes
                "title": title or "",
                "visit_duration": visit_duration,
                "transition": _decode_transition(transition_val),
                "typed_count": typed_cnt,
                "visit_count": visit_cnt,
                "referrer": referrer_url,
            }

            # Filter empty values for the mandatory XES attributes
            if ev["case:concept:name"].strip() and ev["concept:name"].strip():
                events.append(ev)

        cur.close()
        conn.close()

    # ---------------------------------------------------------------------
    # Build PM4Py dataframe (same post‑processing as original)
    # ---------------------------------------------------------------------
    df = pandas_utils.instantiate_dataframe(events)
    if not df.empty:
        df = pandas_utils.insert_index(df, "@@index", copy_dataframe=False, reset_index=False)
        df = df.sort_values(["time:timestamp", "@@index"])
        df["@@case_index"] = df.groupby("case:concept:name", sort=False).ngroup()
        df = df.sort_values(["@@case_index", "time:timestamp", "@@index"])
    return df
