from __future__ import annotations
import argparse
import os
import sys
from typing import Any, Dict, List, Tuple

from .normalizer import normalize
from .policy import Policy
from .scanner import scan
from .actions import apply_action
from .audit import write_event, now_iso
from .dirpack import pack_dir, unpack_dir
from .storage import Vault

def _validate_input_path(path: str, arg_name: str = "input") -> None:
    """Validate that an input path exists (or is stdin)."""
    if path == "-":
        return  # stdin is always valid
    if not os.path.exists(path):
        print(f"Error: {arg_name} path does not exist: {path}", file=sys.stderr)
        sys.exit(1)
    if not os.path.isfile(path):
        print(f"Error: {arg_name} path is not a file: {path}", file=sys.stderr)
        sys.exit(1)

def _validate_dir_path(path: str, arg_name: str = "directory") -> None:
    """Validate that a directory path exists."""
    if not os.path.exists(path):
        print(f"Error: {arg_name} path does not exist: {path}", file=sys.stderr)
        sys.exit(1)
    if not os.path.isdir(path):
        print(f"Error: {arg_name} path is not a directory: {path}", file=sys.stderr)
        sys.exit(1)

def _validate_policy_path(path: str) -> None:
    """Validate that a policy file exists."""
    if not os.path.exists(path):
        print(f"Error: policy file does not exist: {path}", file=sys.stderr)
        sys.exit(1)
    if not os.path.isfile(path):
        print(f"Error: policy path is not a file: {path}", file=sys.stderr)
        sys.exit(1)

def _load_text(path: str) -> str:
    if path == "-":
        return sys.stdin.read()
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

def _write_text(path: str, text: str) -> None:
    if path == "-":
        sys.stdout.write(text)
        return
    with open(path, "w", encoding="utf-8") as f:
        f.write(text)

def sanitize_text(text: str, policy: Policy, dry_run: bool = False) -> Tuple[str, bool]:
    text = normalize(text)
    matches = scan(text, policy)
    blocked = False
    out = list(text)
    for m in reversed(matches):
        res = apply_action(m.rule, m.value, policy)
        if res.blocked:
            blocked = True
        if not dry_run:
            out[m.start:m.end] = list(res.replacement)
        if policy.globals.audit_enabled:
            write_event(
                policy.globals.audit_path,
                {
                    "ts": now_iso(),
                    "rule_id": m.rule.id,
                    "action": m.rule.action,
                    "blocked": res.blocked,
                    "start": m.start,
                    "end": m.end,
                    "value_hash": res.value_hash,
                },
            )
    return ("".join(out) if not dry_run else text, blocked)

def _print_status(status: Dict[str, Any]) -> None:
    """Print human-readable status report to stderr."""
    print("=" * 50, file=sys.stderr)
    print("CloakMCP Status", file=sys.stderr)
    print("=" * 50, file=sys.stderr)

    # Session
    active = status.get("session_active", False)
    print(f"\nSession: {'ACTIVE' if active else 'INACTIVE'}", file=sys.stderr)
    if active and status.get("session"):
        session = status["session"]
        print(f"  Policy: {session.get('policy', 'N/A')}", file=sys.stderr)
        print(f"  Prefix: {session.get('prefix', 'N/A')}", file=sys.stderr)
        if session.get("backup_path"):
            print(f"  Backup: {session['backup_path']}", file=sys.stderr)

    # Manifest
    manifest = status.get("manifest")
    if manifest:
        print(f"\nManifest: {manifest.get('total_files', 0)} files "
              f"(ts: {manifest.get('timestamp', 'N/A')})", file=sys.stderr)
    else:
        print("\nManifest: none", file=sys.stderr)

    # Delta
    delta = status.get("delta")
    if delta:
        new_files = delta.get("new_files", [])
        deleted_files = delta.get("deleted_files", [])
        changed_files = delta.get("changed_files", [])
        unchanged = delta.get("unchanged_count", 0)
        print(f"\nDelta: {len(new_files)} new, {len(deleted_files)} deleted, "
              f"{len(changed_files)} changed, {unchanged} unchanged", file=sys.stderr)
        for label, files in [("New", new_files), ("Deleted", deleted_files),
                             ("Changed", changed_files)]:
            for f in files[:10]:
                print(f"  [{label}] {f}", file=sys.stderr)
            if len(files) > 10:
                print(f"  ... and {len(files) - 10} more", file=sys.stderr)

    # Vault
    vault = status.get("vault")
    if vault:
        print(f"\nVault: {vault.get('total_secrets', 0)} secrets, "
              f"{vault.get('unique_tags', 0)} unique tags", file=sys.stderr)
        print(f"  Path: {vault.get('vault_path', 'N/A')}", file=sys.stderr)
    else:
        print("\nVault: unavailable", file=sys.stderr)

    # Tag residue
    residue = status.get("tag_residue")
    if residue:
        print(f"\nTag residue: {residue.get('tags_found', 0)} found, "
              f"{residue.get('tags_resolved', 0)} resolved, "
              f"{residue.get('tags_unresolvable', 0)} unresolvable", file=sys.stderr)
        unresolvable_files = residue.get("unresolvable_files", [])
        for rel_path, count in unresolvable_files[:5]:
            print(f"  {rel_path}: {count} tag(s)", file=sys.stderr)
        if len(unresolvable_files) > 5:
            print(f"  ... and {len(unresolvable_files) - 5} more files",
                  file=sys.stderr)

    # Backups
    backups = status.get("backups")
    if backups:
        print(f"\nBackups: {len(backups)} available", file=sys.stderr)
        for b in backups[:5]:
            print(f"  {b['timestamp']}  ({b['file_count']} files)", file=sys.stderr)
        if len(backups) > 5:
            print(f"  ... and {len(backups) - 5} more", file=sys.stderr)
    else:
        print("\nBackups: none", file=sys.stderr)

    # Legacy warning
    legacy = status.get("legacy_warning")
    if legacy:
        print(f"\n{legacy}", file=sys.stderr)

    # Recent audit
    audit = status.get("recent_audit")
    if audit:
        print(f"\nRecent audit ({len(audit)} events):", file=sys.stderr)
        for evt in audit:
            ts = evt.get("ts", "?")
            event_type = evt.get("event", "?")
            print(f"  [{ts}] {event_type}", file=sys.stderr)
    else:
        print("\nAudit: no events", file=sys.stderr)

    print("", file=sys.stderr)


def main() -> None:
    p = argparse.ArgumentParser(prog="cloak", description="Micro-Cleanse Preprocessor (local secret-removal)")
    sub = p.add_subparsers(dest="cmd", required=True)

    # ── Existing commands ───────────────────────────────────────

    s_scan = sub.add_parser("scan", help="Scan input and log detections (no modifications)")
    s_scan.add_argument("--policy", required=True)
    s_scan.add_argument("--input", required=True, help="File path or '-' for stdin")

    s_san = sub.add_parser("sanitize", help="Sanitize input and write output")
    s_san.add_argument("--policy", required=True)
    s_san.add_argument("--input", required=True, help="File path or '-' for stdin")
    s_san.add_argument("--output", required=True, help="File path or '-' for stdout")

    s_pack = sub.add_parser("pack", help="Pack a directory: replace secrets by tags (vaulted)")
    s_pack.add_argument("--policy", required=True)
    s_pack.add_argument("--dir", required=True, help="Directory to process")
    s_pack.add_argument("--prefix", default="TAG", help="Tag prefix (e.g., TAG, SEC, KEY)")
    s_pack.add_argument("--dry-run", action="store_true", help="Preview changes without modifying files")
    s_pack.add_argument("--no-backup", action="store_true", help="Disable automatic backup (not recommended)")

    s_unpack = sub.add_parser("unpack", help="Unpack a directory: restore tags from vault")
    s_unpack.add_argument("--dir", required=True, help="Directory to process")
    s_unpack.add_argument("--dry-run", action="store_true", help="Preview changes without modifying files")
    s_unpack.add_argument("--no-backup", action="store_true", help="Disable automatic backup (not recommended)")

    s_vault_export = sub.add_parser("vault-export", help="Export vault to encrypted backup file")
    s_vault_export.add_argument("--dir", required=True, help="Project directory")
    s_vault_export.add_argument("--output", required=True, help="Output file path")

    s_vault_import = sub.add_parser("vault-import", help="Import vault from encrypted backup file")
    s_vault_import.add_argument("--dir", required=True, help="Project directory")
    s_vault_import.add_argument("--input", required=True, help="Input file path")

    s_vault_stats = sub.add_parser("vault-stats", help="Display vault statistics")
    s_vault_stats.add_argument("--dir", required=True, help="Project directory")

    # Policy management commands
    s_policy = sub.add_parser("policy", help="Policy management (validate, show merged policy)")
    policy_sub = s_policy.add_subparsers(dest="policy_cmd", required=True)

    s_policy_validate = policy_sub.add_parser("validate", help="Validate policy file (including inheritance)")
    s_policy_validate.add_argument("--policy", required=True, help="Policy file to validate")

    s_policy_show = policy_sub.add_parser("show", help="Show merged policy (after inheritance)")
    s_policy_show.add_argument("--policy", required=True, help="Policy file to show")
    s_policy_show.add_argument("--format", choices=["yaml", "json"], default="yaml", help="Output format")

    # ── v0.4.0: File-level pack/unpack ──────────────────────────

    s_pack_file = sub.add_parser("pack-file", help="Pack a single file: replace secrets by tags")
    s_pack_file.add_argument("--policy", required=True)
    s_pack_file.add_argument("--file", required=True, help="File to process")
    s_pack_file.add_argument("--prefix", default="TAG", help="Tag prefix")
    s_pack_file.add_argument("--project-root", default=".", help="Project root for vault lookup")
    s_pack_file.add_argument("--dry-run", action="store_true", help="Preview only")

    s_unpack_file = sub.add_parser("unpack-file", help="Unpack a single file: restore tags from vault")
    s_unpack_file.add_argument("--file", required=True, help="File to process")
    s_unpack_file.add_argument("--project-root", default=".", help="Project root for vault lookup")
    s_unpack_file.add_argument("--dry-run", action="store_true", help="Preview only")

    # ── v0.4.0: Guard (stdin secret scanner) ────────────────────

    s_guard = sub.add_parser("guard", help="Read stdin, exit 1 if secrets detected")
    s_guard.add_argument("--policy", required=True)

    # ── v0.4.0: Hook dispatcher ─────────────────────────────────

    s_hook = sub.add_parser("hook", help="Handle Claude Code hook events")
    s_hook.add_argument("event", choices=["session-start", "session-end", "guard-write",
                                          "guard-read", "prompt-guard", "safety-guard",
                                          "audit-log"],
                        help="Hook event type")
    s_hook.add_argument("--dir", default=".", help="Project root directory")

    # ── v0.4.0: Recovery ────────────────────────────────────────

    s_recover = sub.add_parser("recover", help="Detect stale session state and run unpack")
    s_recover.add_argument("--dir", default=".", help="Project root directory")

    # ── v0.5.1: stdin sanitization helper ──────────────────────

    s_stdin = sub.add_parser("sanitize-stdin", help="Sanitize text from stdin → stdout")
    s_stdin.add_argument("--policy", required=True, help="Path to YAML policy file")

    # ── v0.6.0: incremental re-pack ─────────────────────────

    s_repack = sub.add_parser("repack", help="Incremental re-pack: scan new/changed files only")
    s_repack.add_argument("--dir", required=True, help="Directory to process")
    s_repack.add_argument("--policy", required=True, help="Path to YAML policy file")
    s_repack.add_argument("--prefix", default="TAG", help="Tag prefix (e.g., TAG, SEC, KEY)")
    s_repack.add_argument("--dry-run", action="store_true", help="Preview changes without modifying files")

    # ── v0.5.1: post-unpack verification ─────────────────────

    s_verify = sub.add_parser("verify", help="Scan for residual tags after unpack")
    s_verify.add_argument("--dir", required=True, help="Directory to verify")

    # ── scripts-path ─────────────────────────────────────────────
    sub.add_parser("scripts-path", help="Print path to bundled installer scripts")

    # ── v0.8.0: session status ─────────────────────────────────────
    s_status = sub.add_parser("status", help="Show session status and diagnostics")
    s_status.add_argument("--dir", default=".", help="Project root directory")
    s_status.add_argument("--json", action="store_true", dest="json_output",
                           help="Output as JSON (machine-readable)")
    s_status.add_argument("--audit-lines", type=int, default=10,
                           help="Number of recent audit events to show (default: 10)")

    # ── v0.8.0: restore ───────────────────────────────────────────
    s_restore = sub.add_parser("restore", help="Restore secrets (vault-based or from backup)")
    s_restore.add_argument("--dir", default=".", help="Project root directory")
    s_restore.add_argument("--from-backup", action="store_true",
                            help="Restore from external backup instead of vault")
    s_restore.add_argument("--force", action="store_true",
                            help="Execute destructive backup restore (required with --from-backup)")
    s_restore.add_argument("--backup-id", default=None,
                            help="Timestamp of specific backup to restore from")

    # ── Dispatch ────────────────────────────────────────────────

    args = p.parse_args()
    if args.cmd == "scan":
        _validate_policy_path(args.policy)
        _validate_input_path(args.input, "input")
        policy = Policy.load(args.policy)
        text = _load_text(args.input)
        _ = sanitize_text(text, policy, dry_run=True)
        return

    if args.cmd == "sanitize":
        _validate_policy_path(args.policy)
        _validate_input_path(args.input, "input")
        policy = Policy.load(args.policy)
        text = _load_text(args.input)
        out, blocked = sanitize_text(text, policy, dry_run=False)
        if blocked:
            print("ERROR: one or more blocked secrets detected; refusing to output.", file=sys.stderr)
            sys.exit(2)
        _write_text(args.output, out)
        return

    if args.cmd == "pack":
        _validate_policy_path(args.policy)
        _validate_dir_path(args.dir, "directory")
        policy = Policy.load(args.policy)
        dry_run = getattr(args, 'dry_run', False)
        no_backup = getattr(args, 'no_backup', False)
        pack_dir(args.dir, policy, prefix=args.prefix, in_place=True, dry_run=dry_run, backup=not no_backup)
        return

    if args.cmd == "unpack":
        _validate_dir_path(args.dir, "directory")
        dry_run = getattr(args, 'dry_run', False)
        no_backup = getattr(args, 'no_backup', False)
        unpack_dir(args.dir, dry_run=dry_run, backup=not no_backup)
        return

    if args.cmd == "vault-export":
        _validate_dir_path(args.dir, "directory")
        vault = Vault(args.dir)
        vault.export_to_json(args.output)
        stats = vault.get_stats()
        print(f"Vault exported to {args.output}", file=sys.stderr)
        print(f"  Total secrets: {stats['total_secrets']}", file=sys.stderr)
        print(f"  Vault location: {vault.vault_path}", file=sys.stderr)
        print(f"  Key location: {vault.key_path}", file=sys.stderr)
        return

    if args.cmd == "vault-import":
        _validate_dir_path(args.dir, "directory")
        _validate_input_path(args.input, "input")
        vault = Vault(args.dir)
        vault.import_from_json(args.input)
        stats = vault.get_stats()
        print(f"Vault imported from {args.input}", file=sys.stderr)
        print(f"  Total secrets: {stats['total_secrets']}", file=sys.stderr)
        print(f"  Vault location: {vault.vault_path}", file=sys.stderr)
        return

    if args.cmd == "vault-stats":
        _validate_dir_path(args.dir, "directory")
        vault = Vault(args.dir)
        stats = vault.get_stats()
        print(f"Vault statistics for: {args.dir}", file=sys.stderr)
        print(f"  Project slug: {vault.slug}", file=sys.stderr)
        print(f"  Total secrets: {stats['total_secrets']}", file=sys.stderr)
        print(f"  Unique tags: {stats['unique_tags']}", file=sys.stderr)
        print(f"  Vault location: {vault.vault_path}", file=sys.stderr)
        print(f"  Key location: {vault.key_path}", file=sys.stderr)
        return

    if args.cmd == "policy":
        from .policy import validate_policy, policy_to_yaml
        import json

        if args.policy_cmd == "validate":
            _validate_policy_path(args.policy)
            is_valid, errors = validate_policy(args.policy)
            if is_valid:
                print(f"Policy is valid: {args.policy}", file=sys.stderr)
                policy = Policy.load(args.policy)
                if len(policy._inherits_from) > 1:
                    print(f"  Inheritance chain:", file=sys.stderr)
                    for i, path in enumerate(policy._inherits_from, 1):
                        print(f"    {i}. {path}", file=sys.stderr)
                print(f"  Total detection rules: {len(policy.rules)}", file=sys.stderr)
                sys.exit(0)
            else:
                print(f"Policy validation failed: {args.policy}", file=sys.stderr)
                for error in errors:
                    print(f"  - {error}", file=sys.stderr)
                sys.exit(1)

        elif args.policy_cmd == "show":
            _validate_policy_path(args.policy)
            policy = Policy.load(args.policy)
            if args.format == "yaml":
                print(policy_to_yaml(policy, include_inheritance=True))
            elif args.format == "json":
                from .policy import _policy_to_dict
                data = _policy_to_dict(policy)
                data.pop("_inherits_from", None)  # Remove internal field
                print(json.dumps(data, indent=2))
            return

    # ── v0.4.0 commands ─────────────────────────────────────────

    if args.cmd == "pack-file":
        from .filepack import pack_file
        _validate_policy_path(args.policy)
        _validate_input_path(args.file, "file")
        _validate_dir_path(args.project_root, "project-root")
        policy = Policy.load(args.policy)
        vault = Vault(args.project_root)
        dry_run = getattr(args, 'dry_run', False)
        count = pack_file(args.file, policy, vault, prefix=args.prefix, dry_run=dry_run)
        print(f"{count} secret(s) {'found' if dry_run else 'replaced'} in {args.file}", file=sys.stderr)
        return

    if args.cmd == "unpack-file":
        from .filepack import unpack_file
        _validate_input_path(args.file, "file")
        _validate_dir_path(args.project_root, "project-root")
        vault = Vault(args.project_root)
        dry_run = getattr(args, 'dry_run', False)
        count = unpack_file(args.file, vault, dry_run=dry_run)
        print(f"{count} tag(s) {'found' if dry_run else 'restored'} in {args.file}", file=sys.stderr)
        return

    if args.cmd == "guard":
        _validate_policy_path(args.policy)
        policy = Policy.load(args.policy)
        text = sys.stdin.read()
        norm = normalize(text)
        matches = scan(norm, policy)
        if matches:
            rule_ids = sorted({m.rule.id for m in matches})
            print(
                f"GUARD: {len(matches)} secret(s) detected (rules: {', '.join(rule_ids)})",
                file=sys.stderr,
            )
            sys.exit(1)
        sys.exit(0)

    if args.cmd == "sanitize-stdin":
        _validate_policy_path(args.policy)
        policy = Policy.load(args.policy)
        text = sys.stdin.read()
        out, blocked = sanitize_text(text, policy, dry_run=False)
        if blocked:
            print("ERROR: one or more blocked secrets detected; refusing to output.", file=sys.stderr)
            sys.exit(2)
        sys.stdout.write(out)
        return

    if args.cmd == "repack":
        from .dirpack import repack_dir
        import json as _json
        _validate_policy_path(args.policy)
        _validate_dir_path(args.dir, "directory")
        policy = Policy.load(args.policy)
        # Load session manifest if available (fast path)
        manifest = None
        manifest_path = os.path.join(args.dir, ".cloak-session-manifest.json")
        if os.path.isfile(manifest_path):
            try:
                with open(manifest_path, "r", encoding="utf-8") as f:
                    manifest = _json.load(f)
            except (ValueError, OSError):
                manifest = None
        dry_run = getattr(args, 'dry_run', False)
        result = repack_dir(args.dir, policy, prefix=args.prefix, manifest=manifest, dry_run=dry_run)
        if not dry_run and manifest_path and os.path.isdir(args.dir):
            # Update manifest after repack
            from .dirpack import build_manifest, load_ignores
            ignores = load_ignores(args.dir)
            new_manifest = build_manifest(args.dir, ignores)
            with open(manifest_path, "w", encoding="utf-8") as f:
                _json.dump(new_manifest, f, ensure_ascii=False)
        return

    if args.cmd == "hook":
        from .hooks import dispatch_hook
        dispatch_hook(args.event, project_dir=args.dir)
        return

    if args.cmd == "recover":
        from .hooks import handle_recover
        _validate_dir_path(args.dir, "directory")
        handle_recover(project_dir=args.dir)
        return

    if args.cmd == "status":
        from .hooks import handle_status
        _validate_dir_path(args.dir, "directory")
        result = handle_status(project_dir=args.dir, json_output=args.json_output,
                               audit_lines=args.audit_lines)
        if args.json_output:
            import json as _json
            print(_json.dumps(result, indent=2, default=str))
        else:
            _print_status(result)
        return

    if args.cmd == "restore":
        from .hooks import handle_restore
        _validate_dir_path(args.dir, "directory")
        handle_restore(project_dir=args.dir, from_backup=args.from_backup,
                       force=args.force, backup_id=args.backup_id)
        return

    if args.cmd == "verify":
        from .dirpack import verify_unpack
        _validate_dir_path(args.dir, "directory")
        result = verify_unpack(args.dir)
        print(f"Verification for: {args.dir}", file=sys.stderr)
        print(f"  Tags found:        {result['tags_found']}", file=sys.stderr)
        print(f"  Tags resolved:     {result['tags_resolved']}", file=sys.stderr)
        print(f"  Tags unresolvable: {result['tags_unresolvable']}", file=sys.stderr)
        if result["unresolvable_files"]:
            print("  Files with unresolvable tags:", file=sys.stderr)
            for rel_path, count in result["unresolvable_files"]:
                print(f"    {rel_path}: {count} tag(s)", file=sys.stderr)
        if result["tags_unresolvable"] > 0:
            sys.exit(1)
        sys.exit(0)

    if args.cmd == "scripts-path":
        from importlib.resources import files
        print(files("cloakmcp") / "scripts")
        return

if __name__ == "__main__":
    main()
