# This file is part of Radicale Server - Calendar Server
# Copyright © 2026-2026 Peter Bieringer <pb@bieringer.de>
# Copyright © 2026-2026 Max Berger <max@berger.name>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.

import csv
import json
import os
from typing import Union

from radicale import config, sharing
from radicale.log import logger

""" CVS based sharing by token or map """


class Sharing(sharing.BaseSharing):
    _lines: int = 0
    _sharing_cache: list[dict] = []
    _sharing_db_file: str

    # *** Overloaded functions ***
    def database_init(self) -> bool:
        logger.debug("sharing database initialization for type 'csv'")
        sharing_db_file = self.configuration.get("sharing", "database_path")
        if sharing_db_file == "":
            folder = self.configuration.get("storage", "filesystem_folder")
            folder_db = os.path.join(folder, "collection-db")
            sharing_db_file = os.path.join(folder_db, "sharing.csv")
            logger.info("sharing database filename not provided, use default: %r", sharing_db_file)
        else:
            sharing_db_file = os.path.abspath(sharing_db_file)
            folder_db = os.path.dirname(sharing_db_file)
            logger.info("sharing database filename: %r", sharing_db_file)

        if not os.path.exists(folder_db):
            logger.notice("sharing database folder is not existing: %r (create now)", folder_db)
            try:
                os.mkdir(folder_db)
            except Exception as e:
                logger.error("sharing database folder cannot be created (check permissions): %r (%r)", folder_db, e)
                return False
            logger.notice("sharing database folder successfully created: %r", folder_db)

        if not os.path.exists(sharing_db_file):
            logger.notice("sharing database is not existing: %r", sharing_db_file)
            try:
                if self._create_empty_csv(sharing_db_file) is not True:
                    raise
            except Exception as e:
                logger.error("sharing database (empty) cannot be created (check permissions): %r (%r)", sharing_db_file, e)
                return False
            logger.notice("sharing database (empty) successfully created: %r", sharing_db_file)
        else:
            logger.info("sharing database exists: %r", sharing_db_file)

        # read database
        try:
            if self._load_csv(sharing_db_file) is not True:
                return False
        except Exception as e:
            logger.error("sharing database load failed: %r (%r)", sharing_db_file, e)
            return False
        logger.info("sharing database load successful: %r (lines=%d)", sharing_db_file, self._lines)
        self._sharing_db_file = sharing_db_file
        return True

    def database_get_info(self) -> Union[dict, None]:
        database_info = {'type': "csv"}
        return database_info

    def database_verify(self) -> bool:
        logger.info("sharing database (csv) verification begin")
        logger.info("sharing database (csv) file: %r", self._sharing_db_file)
        logger.info("sharing database (csv) loaded entries: %d", self._lines)
        # nothing more todo for CSV
        logger.info("sharing database (csv) verification end")
        return True

    def database_get_sharing(self,
                             ShareType: str,
                             PathOrToken: str,
                             OnlyEnabled: bool = True,
                             User: Union[str, None] = None) -> Union[dict, None]:
        """ retrieve sharing target and attributes by map """
        # Lookup
        logger.trace("sharing: lookup ShareType=%r PathOrToken=%r User=%r OnlyEnabled=%s)", ShareType, PathOrToken, User, OnlyEnabled)

        index = 0
        found = False
        for row in self._sharing_cache:
            if index == 0:
                # skip fieldnames
                pass
            else:
                logger.trace("sharing: check row: %r", row)
                if row['ShareType'] != ShareType:
                    pass
                elif row['PathOrToken'] != PathOrToken:
                    pass
                elif User is not None and row['User'] != User:
                    pass
                elif OnlyEnabled is True and row['EnabledByOwner'] is not True:
                    pass
                elif OnlyEnabled is True and row['EnabledByUser'] is not True:
                    pass
                else:
                    found = True
                    break
            index += 1

        if found:
            PathMapped = row['PathMapped']
            Owner = row['Owner']
            UserShare = row['User']
            Permissions = row['Permissions']
            Hidden: bool = (row['HiddenByOwner'] or row['HiddenByUser'])
            Properties: Union[dict, None] = None
            Conversion: Union[str, None] = None
            Actions: Union[dict, None] = None
            if 'Properties' in row:
                Properties = row['Properties']
            if 'Conversion' in row:
                Conversion = row['Conversion']
            if 'Actions' in row:
                Actions = row['Actions']
            return {
                    "mapped": True,
                    "ShareType": ShareType,
                    "PathOrToken": PathOrToken,
                    "PathMapped": PathMapped,
                    "Owner": Owner,
                    "User": UserShare,
                    "Hidden": Hidden,
                    "EnabledByOwner": row['EnabledByOwner'],
                    "EnabledByUser": row['EnabledByUser'],
                    "Permissions": Permissions,
                    "Properties": Properties,
                    "Conversion": Conversion,
                    "Actions": Actions,
                    }
        return None

    def database_list_sharing(self,
                              OwnerOrUser: Union[str, None] = None,
                              ShareType: Union[str, None] = None,
                              PathOrToken: Union[str, None] = None,
                              PathMapped: Union[str, None] = None,
                              User: Union[str, None] = None,
                              EnabledByOwner: Union[bool, None] = None,
                              EnabledByUser: Union[bool, None] = None,
                              HiddenByOwner: Union[bool, None] = None,
                              HiddenByUser: Union[bool, None] = None,
                              Conversion: Union[str, None] = None,
                              ) -> list[dict]:
        """ retrieve sharing """
        row: dict
        index = 0
        result = []

        with self._storage.acquire_lock("r", path=self._sharing_db_file):
            logger.trace("sharing/list/called: ShareType=%r OwnerOrUser=%r User=%r PathOrToken=%r PathMapped=%r EnabledByOwner=%s EnabledByUser=%s HiddenByOwner=%s HiddenByUser=%s Conversion=%r", ShareType, OwnerOrUser, User, PathOrToken, PathMapped, EnabledByOwner, EnabledByUser, HiddenByOwner, HiddenByUser, Conversion)

            for row in self._sharing_cache:
                if index == 0:
                    # skip fieldnames
                    pass
                else:
                    logger.trace("sharing/list/row: test: %r", row)
                    if ShareType is not None and row['ShareType'] != ShareType:
                        logger.trace("sharing/list/row: skip by ShareType")
                        pass
                    elif OwnerOrUser is not None and (row['Owner'] != OwnerOrUser and row['User'] != OwnerOrUser):
                        pass
                    elif User is not None and row['User'] != User:
                        logger.trace("sharing/list/row: skip by User")
                        pass
                    elif PathOrToken is not None and row['PathOrToken'] != PathOrToken:
                        logger.trace("sharing/list/row: skip by PathOrToken")
                        pass
                    elif PathMapped is not None and row['PathMapped'] != PathMapped:
                        logger.trace("sharing/list/row: skip by PathMapped")
                        pass
                    elif EnabledByOwner is not None and row['EnabledByOwner'] != EnabledByOwner:
                        pass
                    elif EnabledByUser is not None and row['EnabledByUser'] != EnabledByUser:
                        pass
                    elif HiddenByOwner is not None and row['HiddenByOwner'] != HiddenByOwner:
                        pass
                    elif HiddenByUser is not None and row['HiddenByUser'] != HiddenByUser:
                        pass
                    elif Conversion is not None and row['Conversion'] != Conversion:
                        pass
                    else:
                        logger.trace("sharing/list/row: add : %r", row)
                        result.append(row)
                index += 1
            return result

    def database_create_sharing(self,
                                ShareType: str,
                                PathOrToken: str, PathMapped: str,
                                Conversion: str,
                                Owner: str, User: str,
                                Permissions: str = "r",
                                EnabledByOwner: bool = False, EnabledByUser: bool = False,
                                HiddenByOwner:  bool = True, HiddenByUser:  bool = True,
                                Timestamp: int = 0,
                                Properties: Union[dict, None] = None,
                                Actions: Union[dict, None] = None,
                                ) -> dict:
        """ create sharing """
        row: dict

        with self._storage.acquire_lock("w", path=self._sharing_db_file):
            logger.trace("sharing: ShareType=%r", ShareType)
            if ShareType == "token":
                logger.trace("sharing/token/create: PathOrToken=%r Owner=%r PathMapped=%r User=%r Permissions=%r", PathOrToken, Owner, PathMapped, User, Permissions)
                # check for duplicate token entry
                for row in self._sharing_cache:
                    if row['ShareType'] != "token":
                        continue
                    if row['PathOrToken'] == PathOrToken:
                        # must be unique systemwide
                        logger.error("sharing/token/create: PathOrToken already exists: PathOrToken=%r", PathOrToken)
                        return {"status": "conflict"}
            elif ShareType == "map":
                logger.trace("sharing/map/create: PathOrToken=%r Owner=%r PathMapped=%r User=%r Permissions=%r", PathOrToken, Owner, PathMapped, User, Permissions)
                # check for duplicate map entry
                for row in self._sharing_cache:
                    if row['ShareType'] != "map":
                        continue
                    if row['PathMapped'] == PathMapped and row['User'] == User and row['PathOrToken'] == PathOrToken:
                        # must be unique systemwide
                        logger.error("sharing/map/create: entry already exists: PathMapped=%r User=%r", PathMapped, User)
                        return {"status": "conflict"}
            else:
                return {"status": "error"}

            row = {"ShareType": ShareType,
                   "PathOrToken": PathOrToken,
                   "PathMapped": PathMapped,
                   "Owner": Owner,
                   "User": User,
                   "Permissions": Permissions,
                   "EnabledByOwner": EnabledByOwner,
                   "EnabledByUser": EnabledByUser,
                   "HiddenByOwner": HiddenByOwner,
                   "HiddenByUser": HiddenByUser,
                   "TimestampCreated": Timestamp,
                   "TimestampUpdated": Timestamp,
                   "Properties": Properties,
                   "Conversion": Conversion,
                   "Actions": Actions,
                   }

            logger.trace("sharing/*/create: add row: %r", row)
            self._sharing_cache.append(row)

            if self._write_csv(self._sharing_db_file):
                logger.trace("sharing/%s/create: write CSV done", ShareType)
                return {"status": "success"}

        logger.error("sharing/%s/create: cannot update CSV database", ShareType)
        return {"status": "error"}

    def database_update_sharing(self,
                                ShareType: str,
                                PathOrToken: str,
                                OwnerOrUser: Union[str, None] = None,
                                User: Union[str, None] = None,
                                PathMapped: Union[str, None] = None,
                                Permissions: Union[str, None] = None,
                                EnabledByOwner: Union[bool, None] = None,
                                EnabledByUser:  Union[bool, None] = None,
                                HiddenByOwner:  Union[bool, None] = None,
                                HiddenByUser:   Union[bool, None] = None,
                                Timestamp: int = 0,
                                Properties: Union[dict, None] = None,
                                Conversion: Union[str, None] = None,
                                Actions: Union[dict, None] = None,
                                ) -> dict:
        """ update sharing """
        logger.trace("sharing/%s/update: PathOrToken=%r OwnerOrUser=%r PathMapped=%r Properties=%r EnabledByOwner=%s EnabledByUser=%s HiddenByOwner=%s HiddenByUser=%s", ShareType, PathOrToken, OwnerOrUser, PathMapped, Properties, EnabledByOwner, EnabledByUser, HiddenByOwner, HiddenByUser)

        with self._storage.acquire_lock("w", path=self._sharing_db_file):
            # lookup token
            found = False
            index = 0
            for row in self._sharing_cache:
                if index == 0:
                    # skip fieldnames
                    pass
                if row['ShareType'] != ShareType:
                    pass
                elif row['PathOrToken'] != PathOrToken:
                    pass
                else:
                    found = True
                    break
                index += 1

            if found:
                logger.trace("sharing/%s/update: orig row[%d]=%r", ShareType, index, row)

                # CSV: remove+adjust+readd
                if PathMapped is not None:
                    self._sharing_cache[index]["PathMapped"] = PathMapped
                if Permissions is not None:
                    self._sharing_cache[index]["Permissions"] = Permissions
                if User is not None:
                    self._sharing_cache[index]["User"] = User
                if EnabledByOwner is not None:
                    self._sharing_cache[index]["EnabledByOwner"] = EnabledByOwner
                if EnabledByUser is not None:
                    self._sharing_cache[index]["EnabledByUser"] = EnabledByUser
                if HiddenByOwner is not None:
                    self._sharing_cache[index]["HiddenByOwner"] = HiddenByOwner
                if HiddenByUser is not None:
                    self._sharing_cache[index]["HiddenByUser"] = HiddenByUser
                if Properties is not None:
                    self._sharing_cache[index]["Properties"] = Properties
                if Conversion is not None:
                    self._sharing_cache[index]["Conversion"] = Conversion
                if Actions is not None:
                    self._sharing_cache[index]["Actions"] = Actions
                # update timestamp
                self._sharing_cache[index]["TimestampUpdated"] = Timestamp

                logger.trace("sharing/%s/update: adj  row[%d]=%r", ShareType, index, self._sharing_cache[index])

                if self._write_csv(self._sharing_db_file):
                    logger.trace("sharing/%s/update: write CSV done", ShareType)
                    return {"status": "success"}

                logger.error("sharing/%s/update: cannot update CSV database", ShareType)
                return {"status": "error"}
            else:
                return {"status": "not-found"}

    def database_delete_sharing(self,
                                ShareType: str,
                                PathOrToken: str,
                                User: str) -> dict:
        """ delete sharing """
        logger.trace("sharing/%s/delete: PathOrToken=%r", ShareType, PathOrToken)

        with self._storage.acquire_lock("w", User, path=self._sharing_db_file):
            # lookup token
            found = False
            index = 0
            for row in self._sharing_cache:
                logger.trace("sharing/%s/delete: check: %r", ShareType, row)
                if index == 0:
                    # skip fieldnames
                    pass
                if row['ShareType'] != ShareType:
                    pass
                elif row['PathOrToken'] != PathOrToken:
                    pass
                else:
                    found = True
                    break
                index += 1

            if found:
                logger.trace("sharing/%s/delete: PathOrToken=%r Owner=%r index=%d", ShareType, PathOrToken, row['Owner'], index)
                self._sharing_cache.pop(index)

                if self._write_csv(self._sharing_db_file):
                    logger.trace("sharing/%s/delete: write CSV done", ShareType)
                    return {"status": "success"}

                logger.error("sharing/%s/delete: cannot update CSV database", ShareType)
                return {"status": "error"}
            else:
                return {"status": "not-found"}

    # *** local functions ***
    def _create_empty_csv(self, file: str) -> bool:
        with self._storage.acquire_lock("w", None, path=file):
            with open(file, 'w', newline='') as csvfile:
                writer = csv.DictWriter(csvfile, fieldnames=sharing.DB_FIELDS_V1, delimiter=';')
                writer.writeheader()
        return True

    def _load_csv(self, file: str) -> bool:
        logger.debug("sharing database load begin: %r", file)
        self._sharing_cache = []
        with self._storage.acquire_lock("r", None):
            with open(file, 'r', newline='', encoding=self._encoding) as csvfile:
                reader = csv.DictReader(csvfile, fieldnames=sharing.DB_FIELDS_V1, delimiter=';')
                self._lines = 0
                for row in reader:
                    # logger.debug("sharing database load read: %r", row)
                    if self._lines == 0:
                        # header line, check
                        for fieldname in sharing.DB_FIELDS_V1:
                            logger.debug("sharing database load check fieldname: %r", fieldname)
                            if fieldname not in row:
                                logger.debug("sharing database is incompatible: %r", file)
                                return False
                    # convert txt to bool or int
                    if self._lines > 0:
                        for fieldname in row:
                            logger.trace("sharing/_load_csv: test fieldname=%r", fieldname)
                            if fieldname not in sharing.DB_TYPES_V1:
                                logger.error("sharing database row error, unsupported fieldname found: %r", fieldname)
                                return False
                            if sharing.DB_TYPES_V1[fieldname] is bool:
                                try:
                                    row[fieldname] = config._convert_to_bool(row[fieldname])
                                except Exception as e:
                                    logger.error("sharing database row error in type conversion fieldname=%r row=%r error: %r", fieldname, row, e)
                                    return False
                            elif sharing.DB_TYPES_V1[fieldname] is int:
                                try:
                                    row[fieldname] = int(row[fieldname])
                                except Exception as e:
                                    logger.error("sharing database row error in type conversion fieldname=%r row=%r error: %r", fieldname, row, e)
                                    return False
                            elif sharing.DB_TYPES_V1[fieldname] is dict:
                                if row[fieldname] is None or row[fieldname] == '':
                                    row[fieldname] = {}
                                else:
                                    field = row[fieldname].lstrip('"').rstrip('"')
                                    logger.trace("json prep quote replacer match (before): %s", field)
                                    field = field.replace('"', '\\"').replace("\\'", "'")  # escape "
                                    field = field.replace("{'", '{"')  # replace for JSON start {' -> {"
                                    field = field.replace("'}", '"}')  # replace for JSON end '} -> "}
                                    field = field.replace("': '", '": "')  # replace for JSON entry/value ': ' -> ": "
                                    field = field.replace("', '", '", "')  # replace for JSON delimiter ', ' -> ", "
                                    logger.trace("json prep quote replacer match (after) : %s", field)
                                    try:
                                        row[fieldname] = json.loads(field)
                                    except Exception as e:
                                        logger.error("sharing database row error in type conversion fieldname=%r field=%r row=%r error: %r", fieldname, field, row, e)
                                        return False
                    # check for duplicates
                    for row_cached in self._sharing_cache:
                        if row == row_cached:
                            logger.error("sharing database row duplicate row=%r", row)
                            return False
                    # logger.debug("sharing database load add: %r", row)
                    self._sharing_cache.append(row)
                    self._lines += 1
        logger.debug("sharing database load end: %r", file)
        return True

    def _write_csv(self, file: str) -> bool:
        with open(file, 'w', newline='', encoding=self._encoding) as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=sharing.DB_FIELDS_V1, delimiter=';')
            writer.writerows(self._sharing_cache)
        return True
