"""
Copyright © 2025,2026 Ralph Seichter

This file is part of "stown".

stown 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.

stown 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
stown. If not, see <https://www.gnu.org/licenses/>.
"""

from argparse import Namespace
from enum import Enum
from os import environ
from os import listdir
from os import path
from os import remove as os_remove
from os import stat
from os import symlink
from re import search
from typing import List

from .log import log


class Status(Enum):
    OK = 0
    ERROR = 1  # unspecified error
    ACTION_UNKNOWN = 2
    CRUSH_DEPTH = 3
    FORBIDDEN = 4
    IDENTICAL = 5
    UNEXPECTED_PAIR = 6
    UNSUITABLE_TARGET = 7

    def is_ok(self) -> bool:
        return self.value == Status.OK.value


class Permit(Enum):
    DENIED = 0
    FORCED = 1
    GRANTED = 2
    OVERRIDE = 3

    def is_denied(self) -> bool:
        return self.value == Permit.DENIED.value

    def is_forced(self) -> bool:
        return self.value == Permit.FORCED.value

    def is_granted(self) -> bool:
        return self.value == Permit.GRANTED.value


def fail(message: str, rc: Status = Status.ERROR) -> Status:
    log.error(message)
    return rc


def getenv(key, default=None):
    if key in environ:
        return environ[key]
    return default


def parsed_filename(fn: str, ignore_dot_prefix=False) -> str:
    if not ignore_dot_prefix and fn[0:4] == "dot-":
        return f".{fn[4:]}"
    return fn


def remove(somepath, dry_run=True) -> bool:
    if dry_run:
        suffix = " (dry)"
    else:
        suffix = ""
        os_remove(somepath)
    log.debug(f"removed {somepath}{suffix}")
    return not dry_run


def pathto(somepath, want_abspath: bool, relpath_start=None):
    if want_abspath:
        p = path.abspath(somepath)
    else:
        p = path.relpath(somepath, start=relpath_start)
    return p


def is_same_file(somepath, otherpath) -> bool:
    try:
        sp = stat(otherpath, follow_symlinks=False)
        op = stat(somepath, follow_symlinks=False)
        return sp.st_dev == op.st_dev and sp.st_ino == op.st_ino
    except FileNotFoundError:
        return False


def matches(regex, candidate: str) -> bool:
    if regex and search(regex, candidate):
        log.debug(f"{candidate} matches {regex}")
        return True
    log.debug(f"{candidate} does not match {regex}")
    return False


def is_ignored(args: Namespace, candidate: str) -> bool:
    return matches(args.ignore, candidate)


def obtain_permit(args: Namespace, target, source) -> Permit:
    if is_same_file(target, source):
        per = Permit.DENIED
    elif not path.lexists(target):
        per = Permit.GRANTED
    elif matches(args.override, target):
        per = Permit.OVERRIDE
    elif args.force or args.action == "unlink":
        per = Permit.FORCED
    else:
        per = Permit.DENIED
    log.debug(f"write permit for {target}: {per}")
    return per


def linkto(args: Namespace, target, source) -> Status:
    source = pathto(source, args.absolute, path.dirname(target))
    if obtain_permit(args, target, source).is_denied():
        return fail(f"Target {target} denied", Status.FORBIDDEN)
    if not args.dry_run:
        if path.lexists(target):
            remove(target, args.dry_run)
        if args.action == "link":
            log.info(f"{target} -> {source}")
            symlink(source, target)
        elif args.action != "unlink":
            # Should only happen during unit tests, thanks to argparse.
            return fail(f"Unsupported action: {args.action}", Status.ACTION_UNKNOWN)
    return Status.OK


def is_suitable_target(somepath) -> bool:
    return (not path.exists(somepath)) or path.islink(somepath)


def stown(args: Namespace, target: str, sources: List[str], depth=0, parent_path=None) -> Status:
    if depth >= args.depth:
        return fail(f"Depth limit ({depth}) reached", Status.CRUSH_DEPTH)
    for source in sources:
        if parent_path:
            source = path.join(parent_path, source)
        if not path.exists(source):
            log.warning(f"Skipping {source} (not found)")
            continue
        elif is_ignored(args, source):
            log.warning(f"Ignoring {source}")
            continue
        log.info(f"{'  '*depth}{target} -> {source}")
        if is_same_file(target, source):
            return fail(f"Source {source} and target are identical", Status.IDENTICAL)
        elif path.islink(target):
            rc = linkto(args, target, source)
            if not rc.is_ok():
                return rc
        elif not path.lexists(target):
            return linkto(args, target, source)
        elif path.isdir(source):
            for child in listdir(source):
                tchild = parsed_filename(child, args.no_dot)
                rc = stown(args, path.join(target, tchild), [child], depth + 1, source)
                if not rc.is_ok():  # pragma: no cover
                    return rc
        elif not is_suitable_target(target):
            return fail(f"Unsuitable target {target}", Status.UNSUITABLE_TARGET)
        else:  # pragma: no cover
            return fail(f"Unexpected pair: target {target} and source {source}", Status.UNEXPECTED_PAIR)
    return Status.OK
