from functools import wraps
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Optional

from safety_schemas.models import ConfigModel, ProjectModel
from safety.auth.cli import render_email_note
from safety.cli_util import process_auth_status_not_ready
from safety.console import SafeConsole, main_console
from safety.errors import SafetyException
from safety.scan.main import download_policy, load_policy_file, resolve_policy
from safety.scan.models import ScanOutput
from safety.scan.render import (
    print_announcements,
    print_header,
    print_wait_policy_download,
)
from safety.scan.util import GIT
from ..codebase_utils import load_unverified_project_from_config


from safety.util import build_telemetry_data
from safety_schemas.models import (
    MetadataModel,
    ScanType,
    ReportSchemaVersion,
    PolicySource,
)

LOG = logging.getLogger(__name__)


if TYPE_CHECKING:
    from safety.cli_util import CustomContext
    from safety.models import SafetyCLI

    SafetyContext = CustomContext[SafetyCLI]


def scan_project_command_init(func):
    """
    Decorator to make general verifications before each project scan command.
    """

    @wraps(func)
    def inner(
        ctx: "SafetyContext",
        policy_file_path: Optional[Path],
        target: Path,
        output: ScanOutput,
        console: "SafeConsole" = main_console,
        *args,
        **kwargs,
    ):
        ctx.obj.console = console
        ctx.params.pop("console", None)

        if output.is_silent():
            console.quiet = True

        if not ctx.obj.auth:
            raise SafetyException("Authentication not initialized.")

        if not ctx.obj.auth.is_valid():
            process_auth_status_not_ready(console=console, auth=ctx.obj.auth, ctx=ctx)

        upload_request_id = kwargs.pop("upload_request_id", None)

        # Load .safety-project.ini
        unverified_project = load_unverified_project_from_config(project_root=target)

        print_header(console=console, targets=[target])

        stage = ctx.obj.auth.stage
        platform = ctx.obj.auth.platform
        git_data = GIT(root=target).build_git_data()
        origin = None
        branch = None

        if git_data:
            origin = git_data.origin
            branch = git_data.branch

        if ctx.obj.platform_enabled:
            # TODO: Move this to be injected by a codebase service
            from safety.init.main import verify_project

            link_behavior = "prompt"

            if unverified_project.created:
                link_behavior = "always"

            verify_project(
                console,
                ctx,
                platform,
                unverified_project,
                origin,
                link_behavior=link_behavior,
                prompt_for_name=True,
            )
        else:
            ctx.obj.project = ProjectModel(
                id="",
                name="Undefined project",
                project_path=unverified_project.project_path,
            )

        if not ctx.obj.project:
            raise SafetyException("Codebase not loaded.")

        ctx.obj.project.git = git_data
        ctx.obj.project.upload_request_id = upload_request_id

        if not policy_file_path:
            policy_file_path = target / Path(".safety-policy.yml")

        # Load Policy file and pull it from CLOUD
        local_policy = kwargs.pop("local_policy", load_policy_file(policy_file_path))

        cloud_policy = None
        if ctx.obj.platform_enabled:
            cloud_policy = print_wait_policy_download(
                console,
                (
                    download_policy,
                    {
                        "platform": platform,
                        "project_id": ctx.obj.project.id,
                        "stage": stage,
                        "branch": branch,
                    },
                ),
            )

        ctx.obj.project.policy = resolve_policy(local_policy, cloud_policy)
        config = (
            ctx.obj.project.policy.config
            if ctx.obj.project.policy and ctx.obj.project.policy.config
            else ConfigModel()
        )

        # Preserve global telemetry preference.
        if ctx.obj.config:
            if ctx.obj.config.telemetry_enabled is not None:
                config.telemetry_enabled = ctx.obj.config.telemetry_enabled

        ctx.obj.config = config

        console.print()

        if ctx.obj.auth.org and ctx.obj.auth.org.name:
            console.print(f"[bold]Organization[/bold]: {ctx.obj.auth.org.name}")

        # Display account info based on auth type
        auth_type = ctx.obj.auth.platform.get_authentication_type()
        if auth_type == "api_key":
            details = {"Account": "API key used"}
        elif auth_type == "token":
            content = ctx.obj.auth.email
            if ctx.obj.auth.name != ctx.obj.auth.email:
                content = f"{ctx.obj.auth.name}, {ctx.obj.auth.email}"

            details = {"Account": f"{content} {render_email_note(ctx.obj.auth)}"}
        elif auth_type == "machine_token":
            machine_id = ctx.obj.auth.platform.machine_id or "unknown"
            details = {"Account": f"Machine token (machine:{machine_id})"}
        else:
            details = {"Account": f"Offline - {os.getenv('SAFETY_DB_DIR')}"}

        if ctx.obj.project.id:
            details["Project"] = ctx.obj.project.id

        if ctx.obj.project.git:
            details[" Git branch"] = ctx.obj.project.git.branch  # type: ignore

        details[" Environment"] = ctx.obj.auth.stage if ctx.obj.auth.stage else "-"

        msg = "None, using Safety CLI default policies"

        if ctx.obj.project.policy:
            if ctx.obj.project.policy.source is PolicySource.cloud:
                msg = (
                    "fetched from Safety Platform, "
                    "ignoring any local Safety CLI policy files"
                )
            else:
                if ctx.obj.project.id:
                    msg = f"local {ctx.obj.project.id} project scan policy"
                else:
                    msg = "local scan policy file"

        details[" Scan policy"] = msg

        for k, v in details.items():
            console.print(f"[scan_meta_title]{k}[/scan_meta_title]: {v}")

        print_announcements(console=console, ctx=ctx)

        console.print()

        result = func(ctx, target=target, output=output, *args, **kwargs)

        return result

    return inner


def inject_metadata(func):
    """
    Build metadata per subcommand. A system scan can trigger a project scan,
    the project scan will need to build its own metadata.
    """

    @wraps(func)
    def inner(ctx: "SafetyContext", *args, **kwargs):
        if not ctx.obj.config:
            raise SafetyException("Default configuration not found.")

        if not ctx.obj.auth:
            raise SafetyException("Authentication not initialized.")

        telemetry = build_telemetry_data(
            telemetry=ctx.obj.config.telemetry_enabled,
            command=ctx.command.name,
            subcommand=ctx.invoked_subcommand,
        )

        auth_type = ctx.obj.auth.platform.get_authentication_type()

        scan_type = ScanType(ctx.command.name)
        target = kwargs.get("target", None)
        targets = kwargs.get("targets", None)

        if not scan_type:
            raise SafetyException("Missing scan_type.")

        if scan_type is ScanType.scan:
            if not target:
                raise SafetyException("Missing target.")
            targets = [target]

        metadata = MetadataModel(
            scan_type=scan_type,
            stage=ctx.obj.auth.stage,  # type: ignore
            scan_locations=targets,  # type: ignore
            authenticated=ctx.obj.auth.platform.is_using_auth_credentials(),
            authentication_type=auth_type,  # type: ignore
            telemetry=telemetry,
            schema_version=ReportSchemaVersion.v3_0,
        )

        ctx.obj.schema = ReportSchemaVersion.v3_0
        ctx.obj.metadata = metadata
        ctx.obj.telemetry = telemetry

        return func(ctx, *args, **kwargs)

    return inner
