from __future__ import annotations

import argparse
import json
import logging
import sys

from .config import AppConfig, DEFAULT_CONFIG_FILENAME, write_default_config
from .lock import InstanceLock, InstanceLockError
from .orchestrator import run_multi_watch
from .sync_engine import AuditIssue, RestoreResult, SnapshotRestoreResult, SyncEngine
from .watcher import run_watch_loop

DEFAULT_TARGET = "/Users/den/Library/CloudStorage/SynologyDrive-M/obsidian-sync"
DEFAULT_WINDOWS = r"C:\Users\<user>\SynologyDrive\obsidian-sync"
LOCK_FILENAME = "obsidian-sync.lock"


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="obsidian-sync",
        description="Bidirectional sync between a local Obsidian vault and Synology Drive folder.",
    )
    parser.add_argument(
        "--config",
        default=DEFAULT_CONFIG_FILENAME,
        help="Path to config TOML file (default: obsidian-sync.toml).",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable verbose logging.",
    )

    sub = parser.add_subparsers(dest="command", required=True)

    init_cmd = sub.add_parser("init", help="Create config file and state folders.")
    init_cmd.add_argument("--source", required=True, help="Path to local Obsidian vault.")
    init_cmd.add_argument(
        "--target",
        default=DEFAULT_TARGET,
        help=f"Path to Synology Drive vault mirror (default: {DEFAULT_TARGET}).",
    )
    init_cmd.add_argument(
        "--windows-path",
        default=DEFAULT_WINDOWS,
        help=(
            "Windows Synology local path used for reference/documentation "
            f"(default: {DEFAULT_WINDOWS})."
        ),
    )
    init_cmd.add_argument("--force", action="store_true", help="Overwrite existing config file.")

    sync_now_cmd = sub.add_parser("sync-now", help="Run one sync pass immediately.")
    sync_now_cmd.add_argument("--dry-run", action="store_true", help="Preview actions without writing.")
    sync_now_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    watch_cmd = sub.add_parser("watch", help="Run near-real-time sync loop.")
    watch_cmd.add_argument("--dry-run", action="store_true", help="Preview actions without writing.")
    watch_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    watch_all_cmd = sub.add_parser("watch-all", help="Run watcher for multiple vault configs.")
    watch_all_cmd.add_argument(
        "--configs",
        nargs="+",
        required=True,
        help="One or more config file paths.",
    )
    watch_all_cmd.add_argument("--dry-run", action="store_true", help="Preview actions without writing.")
    watch_all_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    sub.add_parser("status", help="Show config and last sync state.")

    conflicts_cmd = sub.add_parser("conflicts", help="List recent conflict records.")
    conflicts_cmd.add_argument("--limit", type=int, default=20, help="Maximum number of records to show.")
    conflicts_cmd.add_argument("--all", action="store_true", help="Include already resolved conflicts.")

    restore_cmd = sub.add_parser("restore-conflict", help="Restore a conflict copy to source or target.")
    restore_cmd.add_argument("conflict_id", type=int, help="Conflict ID (from `obsidian-sync conflicts`).")
    restore_cmd.add_argument(
        "--side",
        choices=("source", "target"),
        help="Destination side. Default is the losing side recorded for this conflict.",
    )
    restore_cmd.add_argument("--dry-run", action="store_true", help="Preview restore without writing.")
    restore_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    resolve_cmd = sub.add_parser("resolve-conflicts", help="Batch resolve open conflicts by policy.")
    resolve_cmd.add_argument("--policy", choices=("latest", "source", "target"), required=True)
    resolve_cmd.add_argument("--limit", type=int, default=0, help="Maximum conflicts to resolve (0 = no limit).")
    resolve_cmd.add_argument("--dry-run", action="store_true", help="Preview resolution without writing.")
    resolve_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    snapshots_cmd = sub.add_parser("snapshots", help="List snapshot history.")
    snapshots_cmd.add_argument("--limit", type=int, default=50, help="Maximum number of records to show.")
    snapshots_cmd.add_argument("--path", help="Filter by vault-relative path.")
    snapshots_cmd.add_argument("--event-type", help="Filter by event type (snapshot, deleted, conflict_copy, restore_backup).")
    snapshots_cmd.add_argument("--all", action="store_true", help="Include already restored snapshot records.")

    deleted_cmd = sub.add_parser("deleted-files", help="List deleted file recovery records.")
    deleted_cmd.add_argument("--limit", type=int, default=50, help="Maximum number of records to show.")
    deleted_cmd.add_argument("--all", action="store_true", help="Include already restored deleted files.")

    restore_snapshot_cmd = sub.add_parser("restore-snapshot", help="Restore a snapshot record to source or target.")
    restore_snapshot_cmd.add_argument("snapshot_id", type=int, help="Snapshot ID (from `obsidian-sync snapshots`).")
    restore_snapshot_cmd.add_argument(
        "--side",
        choices=("source", "target"),
        help="Destination side. Default uses the side recorded by that snapshot.",
    )
    restore_snapshot_cmd.add_argument("--dry-run", action="store_true", help="Preview restore without writing.")
    restore_snapshot_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    activity_cmd = sub.add_parser("activity", help="Show sync activity log.")
    activity_cmd.add_argument("--limit", type=int, default=100, help="Maximum number of records to show.")
    activity_cmd.add_argument("--path", help="Filter by vault-relative path.")

    health_cmd = sub.add_parser("health", help="Show sync health summary.")
    health_cmd.add_argument("--json", action="store_true", help="Print machine-readable JSON output.")
    health_cmd.add_argument("--fail-on-degraded", action="store_true", help="Return non-zero exit code if health is degraded.")

    audit_cmd = sub.add_parser("audit", help="Run integrity audit between source and target.")
    audit_cmd.add_argument("--repair", action="store_true", help="Run sync repair when issues are found.")
    audit_cmd.add_argument("--dry-run", action="store_true", help="Preview repair without writing.")
    audit_cmd.add_argument("--json", action="store_true", help="Print machine-readable JSON output.")
    audit_cmd.add_argument("--no-lock", action="store_true", help="Disable single-instance lock.")

    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    configure_logging(verbose=args.verbose)

    if args.command == "init":
        return cmd_init(args)
    if args.command == "sync-now":
        return cmd_sync_now(args)
    if args.command == "watch":
        return cmd_watch(args)
    if args.command == "watch-all":
        return cmd_watch_all(args)
    if args.command == "status":
        return cmd_status(args)
    if args.command == "conflicts":
        return cmd_conflicts(args)
    if args.command == "restore-conflict":
        return cmd_restore_conflict(args)
    if args.command == "resolve-conflicts":
        return cmd_resolve_conflicts(args)
    if args.command == "snapshots":
        return cmd_snapshots(args)
    if args.command == "deleted-files":
        return cmd_deleted_files(args)
    if args.command == "restore-snapshot":
        return cmd_restore_snapshot(args)
    if args.command == "activity":
        return cmd_activity(args)
    if args.command == "health":
        return cmd_health(args)
    if args.command == "audit":
        return cmd_audit(args)

    parser.error("Unknown command")
    return 2


def cmd_init(args: argparse.Namespace) -> int:
    config_path = write_default_config(
        args.config,
        source_vault=args.source,
        synology_vault=args.target,
        windows_synology_vault=args.windows_path,
        force=args.force,
    )

    config = AppConfig.load(config_path)
    config.ensure_directories()

    print(f"Created config: {config.config_path}")
    print(f"Source vault:  {config.paths.source_vault}")
    print(f"Synology path: {config.paths.synology_vault}")
    print(f"State DB:      {config.state_db_path}")
    print("")
    print("Next steps:")
    print(f"  obsidian-sync --config {config.config_path} sync-now")
    print(f"  obsidian-sync --config {config.config_path} watch")
    return 0


def cmd_sync_now(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                stats = engine.sync_once(dry_run=args.dry_run)
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 2

    print_sync_stats(stats.to_dict())
    if stats.conflicts:
        print("")
        print(f"Conflicts detected: {stats.conflicts}")
        print("Review with:")
        print(f"  obsidian-sync --config {config.config_path} conflicts")
    return 1 if stats.errors else 0


def cmd_watch(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                initial = engine.sync_once(dry_run=args.dry_run)
                logging.getLogger(__name__).info("Initial sync complete: %s", initial.to_dict())
                run_watch_loop(engine, dry_run=args.dry_run)
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 2

    return 0


def cmd_watch_all(args: argparse.Namespace) -> int:
    try:
        return run_multi_watch(args.configs, dry_run=args.dry_run, no_lock=args.no_lock)
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 2


def cmd_status(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        last_sync_at = engine.state.get_metadata("last_sync_at")
        last_sync_stats = engine.state.get_metadata("last_sync_stats")
        tracked = engine.state.count_files()
        unresolved_conflicts = engine.state.count_conflicts(unresolved_only=True)
        unresolved_deleted = engine.state.count_deleted_files(unresolved_only=True)
        health = engine.health_report()
    finally:
        engine.close()

    print(f"Config:             {config.config_path}")
    print(f"Source vault:       {config.paths.source_vault}")
    print(f"Synology mirror:    {config.paths.synology_vault}")
    print(f"Windows reference:  {config.paths.windows_synology_vault or '(not set)'}")
    print(f"State DB:           {config.state_db_path}")
    print(f"Artifact crypto:    {'enabled' if config.sync.encryption_enabled else 'disabled'}")
    print(f"Vault crypto:       {'enabled' if config.sync.vault_encryption_enabled else 'disabled'}")
    print(f"Tracked files:      {tracked}")
    print(f"Open conflicts:     {unresolved_conflicts}")
    print(f"Deleted recoveries: {unresolved_deleted}")
    print(f"Health:             {health['status']}")
    print(f"Last sync:          {last_sync_at or '(never)'}")

    if last_sync_stats:
        parsed = json.loads(last_sync_stats)
        print("Last sync stats:")
        print_sync_stats(parsed, indent="  ")

    return 0


def cmd_conflicts(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        rows = engine.list_conflicts(limit=max(1, args.limit), include_resolved=args.all)
    finally:
        engine.close()

    if not rows:
        print("No conflict records found.")
        return 0

    for row in rows:
        status = "resolved" if row.restored_at else "open"
        print(
            f"#{row.conflict_id} [{status}] {row.created_at} rel={row.rel_path} "
            f"winner={row.winner_side} loser={row.loser_side}"
        )
        print(f"  copy: {row.conflict_copy_path}")
        if row.restored_at:
            print(f"  resolved: {row.restored_at} (side={row.restored_side})")

    return 0


def cmd_restore_conflict(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                result = engine.restore_conflict(
                    args.conflict_id,
                    side=args.side,
                    dry_run=args.dry_run,
                )
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except (ValueError, FileNotFoundError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        return 2

    print_conflict_restore_result(result)
    print("")
    print("Next step:")
    print(f"  obsidian-sync --config {config.config_path} sync-now")
    return 0


def cmd_resolve_conflicts(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                result = engine.resolve_open_conflicts(
                    policy=args.policy,
                    dry_run=args.dry_run,
                    limit=args.limit if args.limit > 0 else None,
                )
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except (ValueError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        return 2

    print(f"resolved: {result['resolved']}")
    print(f"skipped:  {result['skipped']}")
    print(f"errors:   {result['errors']}")
    return 1 if result["errors"] else 0


def cmd_snapshots(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        rows = engine.list_snapshots(
            limit=max(1, args.limit),
            rel_path=args.path,
            event_type=args.event_type,
            include_restored=args.all,
        )
    finally:
        engine.close()

    if not rows:
        print("No snapshot records found.")
        return 0

    for row in rows:
        status = "resolved" if row.restored_at else "open"
        print(
            f"#{row.snapshot_id} [{status}] {row.created_at} event={row.event_type} "
            f"side={row.side} rel={row.rel_path}"
        )
        print(f"  hash: {row.file_hash}")
        print(f"  file: {row.snapshot_path}")
        if row.restored_at:
            print(f"  resolved: {row.restored_at} (side={row.restored_side})")

    return 0


def cmd_deleted_files(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        rows = engine.list_deleted_files(limit=max(1, args.limit), include_restored=args.all)
    finally:
        engine.close()

    if not rows:
        print("No deleted file records found.")
        return 0

    for row in rows:
        status = "resolved" if row.restored_at else "open"
        print(
            f"#{row.snapshot_id} [{status}] {row.created_at} side={row.side} rel={row.rel_path}"
        )
        print(f"  hash: {row.file_hash}")
        print(f"  file: {row.snapshot_path}")
        if row.restored_at:
            print(f"  resolved: {row.restored_at} (side={row.restored_side})")

    return 0


def cmd_restore_snapshot(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                result = engine.restore_snapshot(
                    args.snapshot_id,
                    side=args.side,
                    dry_run=args.dry_run,
                )
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except (ValueError, FileNotFoundError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        return 2

    print_snapshot_restore_result(result)
    print("")
    print("Next step:")
    print(f"  obsidian-sync --config {config.config_path} sync-now")
    return 0


def cmd_activity(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        rows = engine.list_activity(limit=max(1, args.limit), rel_path=args.path)
    finally:
        engine.close()

    if not rows:
        print("No activity records found.")
        return 0

    for row in rows:
        details = json.dumps(row.details, sort_keys=True)
        print(
            f"#{row.activity_id} {row.created_at} action={row.action} "
            f"side={row.side or '-'} rel={row.rel_path or '-'}"
        )
        print(f"  details: {details}")

    return 0


def cmd_health(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    engine = SyncEngine(config)
    try:
        report = engine.health_report()
    finally:
        engine.close()

    if args.json:
        print(json.dumps(report, sort_keys=True))
    else:
        print(f"status:            {report['status']}")
        print(f"stale:             {report['stale']}")
        print(f"last_sync_at:      {report['last_sync_at']}")
        print(f"age_seconds:       {report['age_seconds']}")
        print(f"open_conflicts:    {report['open_conflicts']}")
        print(f"deleted_recoveries:{report['deleted_recoveries']}")
        print(f"recent_errors_24h: {report['recent_errors_24h']}")
        print(f"encryption_enabled:{report['encryption_enabled']}")
        print(f"vault_crypto:      {report['vault_encryption_enabled']}")

    if args.fail_on_degraded and report["status"] != "ok":
        return 1
    return 0


def cmd_audit(args: argparse.Namespace) -> int:
    config = AppConfig.load(args.config)
    config.ensure_directories()

    try:
        with _instance_lock(config, disabled=args.no_lock):
            engine = SyncEngine(config)
            try:
                issues = engine.audit_integrity(repair=args.repair, dry_run=args.dry_run)
            finally:
                engine.close()
    except InstanceLockError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 2

    if args.json:
        payload = [
            {"issue_type": i.issue_type, "rel_path": i.rel_path, "details": i.details}
            for i in issues
        ]
        print(json.dumps(payload, sort_keys=True))
    else:
        if not issues:
            print("Integrity audit: no issues found.")
        else:
            print(f"Integrity audit found {len(issues)} issue(s):")
            for issue in issues[:200]:
                details = json.dumps(issue.details, sort_keys=True)
                print(f"- {issue.issue_type} rel={issue.rel_path} details={details}")

    return 1 if issues else 0


def print_sync_stats(stats: dict[str, object], *, indent: str = "") -> None:
    keys = [
        "copied_to_source",
        "copied_to_target",
        "deleted_from_source",
        "deleted_from_target",
        "renamed_to_source",
        "renamed_to_target",
        "conflicts",
        "backed_up",
        "skipped",
        "errors",
        "dry_run",
    ]
    for key in keys:
        value = stats.get(key)
        if value is None and key in {"renamed_to_source", "renamed_to_target"}:
            value = 0
        print(f"{indent}{key}: {value}")


def print_conflict_restore_result(result: RestoreResult) -> None:
    print(f"conflict_id: {result.conflict_id}")
    print(f"rel_path:    {result.rel_path}")
    print(f"side:        {result.side}")
    print(f"destination: {result.destination}")
    print(f"dry_run:     {result.dry_run}")
    if result.backup_path:
        print(f"backup_path: {result.backup_path}")


def print_snapshot_restore_result(result: SnapshotRestoreResult) -> None:
    print(f"snapshot_id: {result.snapshot_id}")
    print(f"rel_path:    {result.rel_path}")
    print(f"side:        {result.side}")
    print(f"event_type:  {result.event_type}")
    print(f"destination: {result.destination}")
    print(f"dry_run:     {result.dry_run}")
    if result.backup_path:
        print(f"backup_path: {result.backup_path}")


def configure_logging(*, verbose: bool) -> None:
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(name)s - %(message)s",
    )


def _instance_lock(config: AppConfig, *, disabled: bool) -> InstanceLock:
    return InstanceLock(config.state_dir / LOCK_FILENAME, enabled=not disabled)


if __name__ == "__main__":
    sys.exit(main())
