import sys; sys.path.append("..")
import os
import csv
import datetime
import logging
from decimal import Decimal
from collections import defaultdict
from sqlalchemy.dialects.mysql import insert as mysql_insert
from manager.MysqlManager import MysqlManager
from models.base_base import BaseBase
from models.deal_deal import DealDeal
from models.option_opti import OptionOpti

# The logging module is configured so that every message contains a timestamp and a log level for easy troubleshooting.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s: %(message)s"
)

# The variable “CSV_DIR” stores the absolute path to the cache directory that contains all CSV source files.
CSV_DIR = os.path.join("..", "cache")

# The variables below hold the absolute file paths of the four source CSV files used for the import process.
csvFileCategories = os.path.join(CSV_DIR, "categorys.csv")
csvFilePlans      = os.path.join(CSV_DIR, "plans.csv")
csvFileCampaigns  = os.path.join(CSV_DIR, "campaigns.csv")
csvFileOptions    = os.path.join(CSV_DIR, "options.csv")

# This helper function returns the content of a semicolon‑separated CSV file as a list of dictionaries.
def read_csv(path):

    # The file is opened with UTF‑8 encoding so non‑ASCII characters are parsed correctly.
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f, delimiter=";"))

# The four CSV files are loaded into separate lists so their rows are available for further processing.
cat_rows  = read_csv(csvFileCategories)
plan_rows = read_csv(csvFilePlans)
camp_rows = read_csv(csvFileCampaigns)
opt_rows  = read_csv(csvFileOptions)

# The dictionary “category_name” maps every category id to its human‑readable name for quick look‑ups.
category_name = {r["id"].strip(): r["name"].strip() for r in cat_rows}

# The default dictionary “campaigns_by_plan” groups all campaign rows by the id of their related plan.
campaigns_by_plan = defaultdict(list)
for c in camp_rows:

    # Each campaign is appended to the list identified by its plan id so multiple campaigns can share the same key.
    campaigns_by_plan[c["plan"].strip()].append(c)

# The default dictionary “options_by_plan” groups all option rows by the id of their related plan.
options_by_plan = defaultdict(list)
for o in opt_rows:

    # Each option is appended to the list identified by its plan id so multiple options can share the same key.
    options_by_plan[o["plan"].strip()].append(o)

# The variable “mysql” establishes the database connection manager so sessions can be created.
mysql   = MysqlManager()

# The variable “session” stores the SQLAlchemy session that will be used for all database operations in this run.
session = mysql.getSession()

# The dictionary “base_db” maps a tuple of provider name and provider code to the corresponding BaseBase object.
base_db = {(b.provider_base, b.providercode_base): b for b in session.query(BaseBase).all()}

# The default dictionary “deals_db” groups all existing DealDeal rows by their base id for quick comparison later on.
deals_db = defaultdict(dict)
for d in session.query(DealDeal).all():

    # Each deal is added to the inner dictionary that is addressed by the base id so provider codes become the second‑level keys.
    deals_db[d.base_deal][d.providercode_deal] = d

# The default dictionary “opts_db” groups all existing OptionOpti rows by their base id for quick comparison later on.
opts_db = defaultdict(dict)
for o in session.query(OptionOpti).all():

    # Each option is added to the inner dictionary that is addressed by the base id so provider codes become the second‑level keys.
    opts_db[o.base_opti][o.providercode_opti] = o

# The variable “now” stores the current timestamp so all new rows share an identical creation and update time.
now = datetime.datetime.now()

# The list “new_bases” collects BaseBase objects that need to be inserted because they do not yet exist in the database.
new_bases = []
for p in plan_rows:

    # The variable “prov_base” combines several CSV columns to form the provider_base value used in the database.
    prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"

    # The variable “key” uniquely identifies a base by provider_base and provider code so duplicates can be detected.
    key = (prov_base, p["id"].strip())

    # This branch creates a new BaseBase object when the combination of provider_base and provider code is unknown.
    if key not in base_db:

        # The variable “b” stores the new BaseBase object that is populated with basic attributes and timestamps.
        b = BaseBase(
            provider_base     = prov_base,
            providercode_base = p["id"].strip(),
            name_base         = p["name"].strip(),
            created_base      = now,
            updated_base      = now
        )
        new_bases.append(b)
        base_db[key] = b

# This branch writes all new BaseBase objects to the database in one bulk operation and refreshes their primary keys.
if new_bases:
    session.add_all(new_bases)
    session.flush()
    logging.info("Inserted %d new bases", len(new_bases))

# The default dictionaries below keep track of all deals and options that should exist after the import run.
desired_deals = defaultdict(set)
desired_opts  = defaultdict(set)

# The two lists below accumulate dictionaries that will later be used for INSERT IGNORE bulk operations.
deal_rows_insert = []
opt_rows_insert  = []
for p in plan_rows:

    # The variable “prov_base” recomputes the provider_base value so it matches the previously used key.
    prov_base = f"Freenet | {p['provider'].strip()} | {p['rahmen'].strip()}"

    # The variable “base_obj” retrieves the corresponding BaseBase object from the in‑memory cache.
    base_obj  = base_db[(prov_base, p["id"].strip())]

    # The variable “b_id” stores the primary key of the current BaseBase row.
    b_id      = base_obj.id_base

    # The variable “price” converts the plan price from a string into a Decimal object for precise arithmetic.
    price     = Decimal(p["price"].strip() or "0.00")

    # The mandatory default deal without campaign code is added to the desired set and the insert list.
    desired_deals[b_id].add("")
    deal_rows_insert.append({
        "provisiongroup_deal": 1,
        "base_deal":           b_id,
        "providercode_deal":   "",
        "name_deal":           "",
        "price_deal":          price,
        "starts_deal":         now,
        "stops_deal":          None,
        "created_deal":        now,
        "updated_deal":        now
    })

    # Every campaign row belonging to the current plan is processed and prepared for insertion.
    for c in campaigns_by_plan[p["id"].strip()]:

        # The variable “code” prefixes the campaign id with “A” so it conforms to the provider_code format.
        code = f"A{c['id'].strip()}"
        desired_deals[b_id].add(code)
        deal_rows_insert.append({
            "provisiongroup_deal": 1,
            "base_deal":           b_id,
            "providercode_deal":   code,
            "name_deal":           c["name"].strip(),
            "price_deal":          price,
            "starts_deal":         now,
            "stops_deal":          None,
            "created_deal":        now,
            "updated_deal":        now
        })

    # Every option row belonging to the current plan is processed and prepared for insertion.
    for o in options_by_plan[p["id"].strip()]:

        # The variable “code_opt” holds the raw option id string so it can be used as provider code.
        code_opt = o["id"].strip()
        desired_opts[b_id].add(code_opt)
        opt_rows_insert.append({
            "provisiongroup_opti":   1,
            "base_opti":             b_id,
            "providercode_opti":     code_opt,
            "providercategory_opti": o["category"].strip(),
            "name_opti":             f"{category_name.get(o['category'].strip(),'Unbekannt')} | {o['name'].strip()}",
            "alias_opti":            None,
            "price_opti":            Decimal(o["price"].strip() or "0.00"),
            "starts_opti":           now,
            "stops_opti":            None,
            "provision1_opti":       Decimal("0.00000"),
            "provision2_opti":       Decimal("0.00000"),
            "provision3_opti":       Decimal("0.00000"),
            "provision4_opti":       Decimal("0.00000"),
            "created_opti":          now,
            "updated_opti":          now
        })

# Duplicate deals are removed by transforming the list into a dictionary keyed by base id and provider code.
deal_rows_insert = list({(r["base_deal"], r["providercode_deal"]): r for r in deal_rows_insert}.values())

# Duplicate options are removed by transforming the list into a dictionary keyed by base id, provider code, and category.
opt_rows_insert = list({
    (r["base_opti"], r["providercode_opti"], r["providercategory_opti"]): r for r in opt_rows_insert
}.values())

# The lists below collect maps that will later be used to stop or reactivate existing deals and options.
stop_deals, react_deals = [], []
stop_opts,  react_opts  = [], []
for (prov, _), b in base_db.items():

    # This branch skips bases that were not imported from Freenet.
    if not prov.startswith("Freenet"):
        continue

    # The variable “b_id” stores the primary key of the base row currently being checked.
    b_id  = b.id_base

    # The variables “wantD” and “haveD” hold the sets of desired and existing deals for the current base.
    wantD = desired_deals.get(b_id, set())
    haveD = deals_db.get(b_id, {})

    # This loop checks every existing deal so its stop status can be updated when necessary.
    for code, obj in haveD.items():

        # This branch marks a stopped deal for reactivation when the code is desired but the stop date is set.
        if code in wantD and obj.stops_deal is not None:
            react_deals.append({"id_deal": obj.id_deal, "stops_deal": None, "updated_deal": now})

        # This branch marks an active deal for stopping when the code is no longer desired.
        if code not in wantD and obj.stops_deal is None:
            stop_deals.append({"id_deal": obj.id_deal, "stops_deal": now, "updated_deal": now})

    # The variables “wantO” and “haveO” hold the sets of desired and existing options for the current base.
    wantO = desired_opts.get(b_id, set())
    haveO = opts_db.get(b_id, {})

    # This loop checks every existing option so its stop status can be updated when necessary.
    for code, obj in haveO.items():

        # This branch marks a stopped option for reactivation when the code is desired but the stop date is set.
        if code in wantO and obj.stops_opti is not None:
            react_opts.append({"id_opti": obj.id_opti, "stops_opti": None, "updated_opti": now})

        # This branch marks an active option for stopping when the code is no longer desired.
        if code not in wantO and obj.stops_opti is None:
            stop_opts.append({"id_opti": obj.id_opti, "stops_opti": now, "updated_opti": now})

# All prepared deal rows are written to the database using INSERT IGNORE so duplicates are silently skipped.
session.execute(mysql_insert(DealDeal.__table__).prefix_with("IGNORE"), deal_rows_insert)
logging.info("INSERT IGNORE'd %d deals", len(deal_rows_insert))

# All prepared option rows are written to the database using INSERT IGNORE so duplicates are silently skipped.
session.execute(mysql_insert(OptionOpti.__table__).prefix_with("IGNORE"), opt_rows_insert)
logging.info("INSERT IGNORE'd %d options", len(opt_rows_insert))

# All deals marked for stopping are updated in bulk so their stop dates are set in a single efficient query.
if stop_deals:
    session.bulk_update_mappings(DealDeal, stop_deals)
    logging.info("Stopped    %d deals", len(stop_deals))

# All deals marked for reactivation are updated in bulk so their stop dates are cleared in a single efficient query.
if react_deals:
    session.bulk_update_mappings(DealDeal, react_deals)
    logging.info("Reactivated %d deals", len(react_deals))

# All options marked for stopping are updated in bulk so their stop dates are set in a single efficient query.
if stop_opts:
    session.bulk_update_mappings(OptionOpti, stop_opts)
    logging.info("Stopped    %d options", len(stop_opts))

# All options marked for reactivation are updated in bulk so their stop dates are cleared in a single efficient query.
if react_opts:
    session.bulk_update_mappings(OptionOpti, react_opts)
    logging.info("Reactivated %d options", len(react_opts))

# The commit call persists every change made in this run so all inserts and updates become permanent.
session.commit()

# The session is closed to release database connections and other resources held by SQLAlchemy.
session.close()
logging.info("Import run finished successfully.")